3D Print Toolbox: Add hollow out #105194
@ -13,12 +13,13 @@ from bpy.props import (
|
|||||||
EnumProperty,
|
EnumProperty,
|
||||||
FloatProperty,
|
FloatProperty,
|
||||||
StringProperty,
|
StringProperty,
|
||||||
|
CollectionProperty,
|
||||||
)
|
)
|
||||||
import bpy
|
import bpy
|
||||||
bl_info = {
|
bl_info = {
|
||||||
"name": "Autodesk 3DS format",
|
"name": "Autodesk 3DS format",
|
||||||
"author": "Bob Holcomb, Campbell Barton, Sebastian Schrand",
|
"author": "Bob Holcomb, Campbell Barton, Sebastian Schrand",
|
||||||
"version": (2, 4, 9),
|
"version": (2, 5, 0),
|
||||||
"blender": (4, 1, 0),
|
"blender": (4, 1, 0),
|
||||||
"location": "File > Import-Export",
|
"location": "File > Import-Export",
|
||||||
"description": "3DS Import/Export meshes, UVs, materials, textures, "
|
"description": "3DS Import/Export meshes, UVs, materials, textures, "
|
||||||
@ -46,6 +47,9 @@ class Import3DS(bpy.types.Operator, ImportHelper):
|
|||||||
|
|
||||||
filename_ext = ".3ds"
|
filename_ext = ".3ds"
|
||||||
filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'})
|
filter_glob: StringProperty(default="*.3ds", options={'HIDDEN'})
|
||||||
|
filepath: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE'})
|
||||||
|
files: CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
|
||||||
|
directory: StringProperty(subtype='DIR_PATH')
|
||||||
|
|
||||||
constrain_size: FloatProperty(
|
constrain_size: FloatProperty(
|
||||||
name="Constrain Size",
|
name="Constrain Size",
|
||||||
@ -106,7 +110,6 @@ class Import3DS(bpy.types.Operator, ImportHelper):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
from . import import_3ds
|
from . import import_3ds
|
||||||
|
|
||||||
keywords = self.as_keywords(ignore=("axis_forward",
|
keywords = self.as_keywords(ignore=("axis_forward",
|
||||||
"axis_up",
|
"axis_up",
|
||||||
"filter_glob",
|
"filter_glob",
|
||||||
@ -123,6 +126,17 @@ class Import3DS(bpy.types.Operator, ImportHelper):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MAX3DS_FH_import(bpy.types.FileHandler):
|
||||||
|
bl_idname = "MAX3DS_FH_import"
|
||||||
|
bl_label = "File handler for 3ds import"
|
||||||
|
bl_import_operator = "import_scene.max3ds"
|
||||||
|
bl_file_extensions = ".3ds;.3DS"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll_drop(cls, context):
|
||||||
|
return (context.area and context.area.type == 'VIEW_3D')
|
||||||
|
|
||||||
|
|
||||||
class MAX3DS_PT_import_include(bpy.types.Panel):
|
class MAX3DS_PT_import_include(bpy.types.Panel):
|
||||||
bl_space_type = 'FILE_BROWSER'
|
bl_space_type = 'FILE_BROWSER'
|
||||||
bl_region_type = 'TOOL_PROPS'
|
bl_region_type = 'TOOL_PROPS'
|
||||||
@ -346,6 +360,7 @@ def menu_func_import(self, context):
|
|||||||
|
|
||||||
def register():
|
def register():
|
||||||
bpy.utils.register_class(Import3DS)
|
bpy.utils.register_class(Import3DS)
|
||||||
|
bpy.utils.register_class(MAX3DS_FH_import)
|
||||||
bpy.utils.register_class(MAX3DS_PT_import_include)
|
bpy.utils.register_class(MAX3DS_PT_import_include)
|
||||||
bpy.utils.register_class(MAX3DS_PT_import_transform)
|
bpy.utils.register_class(MAX3DS_PT_import_transform)
|
||||||
bpy.utils.register_class(Export3DS)
|
bpy.utils.register_class(Export3DS)
|
||||||
@ -357,6 +372,7 @@ def register():
|
|||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
bpy.utils.unregister_class(Import3DS)
|
bpy.utils.unregister_class(Import3DS)
|
||||||
|
bpy.utils.unregister_class(MAX3DS_FH_import)
|
||||||
bpy.utils.unregister_class(MAX3DS_PT_import_include)
|
bpy.utils.unregister_class(MAX3DS_PT_import_include)
|
||||||
bpy.utils.unregister_class(MAX3DS_PT_import_transform)
|
bpy.utils.unregister_class(MAX3DS_PT_import_transform)
|
||||||
bpy.utils.unregister_class(Export3DS)
|
bpy.utils.unregister_class(Export3DS)
|
||||||
|
@ -713,6 +713,7 @@ def make_material_chunk(material, image):
|
|||||||
material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha))
|
material_chunk.add_subchunk(make_percent_subchunk(MATTRANS, 1 - wrap.alpha))
|
||||||
material_chunk.add_subchunk(make_percent_subchunk(MATXPFALL, wrap.transmission))
|
material_chunk.add_subchunk(make_percent_subchunk(MATXPFALL, wrap.transmission))
|
||||||
material_chunk.add_subchunk(make_percent_subchunk(MATSELFILPCT, wrap.emission_strength))
|
material_chunk.add_subchunk(make_percent_subchunk(MATSELFILPCT, wrap.emission_strength))
|
||||||
|
if wrap.node_principled_bsdf is not None:
|
||||||
material_chunk.add_subchunk(make_percent_subchunk(MATREFBLUR, wrap.node_principled_bsdf.inputs['Coat Weight'].default_value))
|
material_chunk.add_subchunk(make_percent_subchunk(MATREFBLUR, wrap.node_principled_bsdf.inputs['Coat Weight'].default_value))
|
||||||
material_chunk.add_subchunk(shading)
|
material_chunk.add_subchunk(shading)
|
||||||
|
|
||||||
|
@ -1671,10 +1671,12 @@ def load_3ds(filepath, context, CONSTRAIN=10.0, UNITS=False, IMAGE_SEARCH=True,
|
|||||||
object_dictionary.clear()
|
object_dictionary.clear()
|
||||||
object_matrix.clear()
|
object_matrix.clear()
|
||||||
|
|
||||||
|
"""
|
||||||
if APPLY_MATRIX:
|
if APPLY_MATRIX:
|
||||||
for ob in imported_objects:
|
for ob in imported_objects:
|
||||||
if ob.type == 'MESH':
|
if ob.type == 'MESH':
|
||||||
ob.data.transform(ob.matrix_local.inverted())
|
ob.data.transform(ob.matrix_local.inverted())
|
||||||
|
"""
|
||||||
|
|
||||||
if UNITS:
|
if UNITS:
|
||||||
unit_mtx = mathutils.Matrix.Scale(MEASURE,4)
|
unit_mtx = mathutils.Matrix.Scale(MEASURE,4)
|
||||||
@ -1768,11 +1770,12 @@ def load_3ds(filepath, context, CONSTRAIN=10.0, UNITS=False, IMAGE_SEARCH=True,
|
|||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
def load(operator, context, filepath="", constrain_size=0.0, use_scene_unit=False,
|
def load(operator, context, files=None, directory="", filepath="", constrain_size=0.0, use_scene_unit=False,
|
||||||
use_image_search=True, object_filter=None, use_world_matrix=False, use_keyframes=True,
|
use_image_search=True, object_filter=None, use_world_matrix=False, use_keyframes=True,
|
||||||
use_apply_transform=True, global_matrix=None, use_cursor=False, use_center_pivot=False):
|
use_apply_transform=True, global_matrix=None, use_cursor=False, use_center_pivot=False):
|
||||||
|
|
||||||
load_3ds(filepath, context, CONSTRAIN=constrain_size, UNITS=use_scene_unit,
|
for f in files:
|
||||||
|
load_3ds(os.path.join(directory, f.name), context, CONSTRAIN=constrain_size, UNITS=use_scene_unit,
|
||||||
IMAGE_SEARCH=use_image_search, FILTER=object_filter, WORLD_MATRIX=use_world_matrix, KEYFRAME=use_keyframes,
|
IMAGE_SEARCH=use_image_search, FILTER=object_filter, WORLD_MATRIX=use_world_matrix, KEYFRAME=use_keyframes,
|
||||||
APPLY_MATRIX=use_apply_transform, CONVERSE=global_matrix, CURSOR=use_cursor, PIVOT=use_center_pivot,)
|
APPLY_MATRIX=use_apply_transform, CONVERSE=global_matrix, CURSOR=use_cursor, PIVOT=use_center_pivot,)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
bl_info = {
|
bl_info = {
|
||||||
'name': 'glTF 2.0 format',
|
'name': 'glTF 2.0 format',
|
||||||
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors',
|
||||||
"version": (4, 2, 0),
|
"version": (4, 2, 5),
|
||||||
'blender': (4, 1, 0),
|
'blender': (4, 1, 0),
|
||||||
'location': 'File > Import-Export',
|
'location': 'File > Import-Export',
|
||||||
'description': 'Import-Export as glTF 2.0',
|
'description': 'Import-Export as glTF 2.0',
|
||||||
@ -121,6 +121,27 @@ def on_export_action_filter_changed(self, context):
|
|||||||
del bpy.types.Scene.gltf_action_filter_active
|
del bpy.types.Scene.gltf_action_filter_active
|
||||||
|
|
||||||
|
|
||||||
|
def get_format_items(scene, context):
|
||||||
|
|
||||||
|
|
||||||
|
items = (('GLB', 'glTF Binary (.glb)',
|
||||||
|
'Exports a single file, with all data packed in binary form. '
|
||||||
|
'Most efficient and portable, but more difficult to edit later'),
|
||||||
|
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
|
||||||
|
'Exports multiple files, with separate JSON, binary and texture data. '
|
||||||
|
'Easiest to edit later'))
|
||||||
|
|
||||||
|
if bpy.context.preferences.addons['io_scene_gltf2'].preferences \
|
||||||
|
and "allow_embedded_format" in bpy.context.preferences.addons['io_scene_gltf2'].preferences \
|
||||||
|
and bpy.context.preferences.addons['io_scene_gltf2'].preferences['allow_embedded_format']:
|
||||||
|
# At initialization, the preferences are not yet loaded
|
||||||
|
# The second line check is needed until the PR is merge in Blender, for github CI tests
|
||||||
|
items += (('GLTF_EMBEDDED', 'glTF Embedded (.gltf)',
|
||||||
|
'Exports a single file, with all data packed in JSON. '
|
||||||
|
'Less efficient than binary, but easier to edit later'
|
||||||
|
),)
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
class ConvertGLTF2_Base:
|
class ConvertGLTF2_Base:
|
||||||
@ -254,17 +275,12 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
|
|
||||||
export_format: EnumProperty(
|
export_format: EnumProperty(
|
||||||
name='Format',
|
name='Format',
|
||||||
items=(('GLB', 'glTF Binary (.glb)',
|
items=get_format_items,
|
||||||
'Exports a single file, with all data packed in binary form. '
|
|
||||||
'Most efficient and portable, but more difficult to edit later'),
|
|
||||||
('GLTF_SEPARATE', 'glTF Separate (.gltf + .bin + textures)',
|
|
||||||
'Exports multiple files, with separate JSON, binary and texture data. '
|
|
||||||
'Easiest to edit later')),
|
|
||||||
description=(
|
description=(
|
||||||
'Output format. Binary is most efficient, '
|
'Output format. Binary is most efficient, '
|
||||||
'but JSON may be easier to edit later'
|
'but JSON may be easier to edit later'
|
||||||
),
|
),
|
||||||
default='GLB', #Warning => If you change the default, need to change the default filter too
|
default=0, #Warning => If you change the default, need to change the default filter too
|
||||||
update=on_export_format_changed,
|
update=on_export_format_changed,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -584,6 +600,10 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
'Export actions (actives and on NLA tracks) as separate animations'),
|
'Export actions (actives and on NLA tracks) as separate animations'),
|
||||||
('ACTIVE_ACTIONS', 'Active actions merged',
|
('ACTIVE_ACTIONS', 'Active actions merged',
|
||||||
'All the currently assigned actions become one glTF animation'),
|
'All the currently assigned actions become one glTF animation'),
|
||||||
|
('BROADCAST', 'Broadcast actions',
|
||||||
|
'Broadcast all compatible actions to all objects. '
|
||||||
|
'Animated objects will get all actions compatible with them, '
|
||||||
|
'others will get no animation at all'),
|
||||||
('NLA_TRACKS', 'NLA Tracks',
|
('NLA_TRACKS', 'NLA Tracks',
|
||||||
'Export individual NLA Tracks as separate animation'),
|
'Export individual NLA Tracks as separate animation'),
|
||||||
('SCENE', 'Scene',
|
('SCENE', 'Scene',
|
||||||
@ -657,6 +677,15 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export_optimize_armature_disable_viewport: BoolProperty(
|
||||||
|
name='Disable viewport if possible',
|
||||||
|
description=(
|
||||||
|
"When exporting armature, disable viewport for other objects, "
|
||||||
|
"for performance. Drivers on shape keys for skined meshes prevent this optimization for now"
|
||||||
|
),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
export_negative_frame: EnumProperty(
|
export_negative_frame: EnumProperty(
|
||||||
name='Negative Frames',
|
name='Negative Frames',
|
||||||
items=(('SLIDE', 'Slide',
|
items=(('SLIDE', 'Slide',
|
||||||
@ -851,6 +880,15 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export_extra_animations: BoolProperty(
|
||||||
|
name='Prepare extra animations',
|
||||||
|
description=(
|
||||||
|
'Export additional animations'
|
||||||
|
'This feature is not standard and needs an external extension to be included in the glTF file'
|
||||||
|
),
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
# Custom scene property for saving settings
|
# Custom scene property for saving settings
|
||||||
scene_key = "glTF2ExportSettings"
|
scene_key = "glTF2ExportSettings"
|
||||||
|
|
||||||
@ -1015,12 +1053,14 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
export_settings['gltf_optimize_animation'] = self.export_optimize_animation_size
|
export_settings['gltf_optimize_animation'] = self.export_optimize_animation_size
|
||||||
export_settings['gltf_optimize_animation_keep_armature'] = self.export_optimize_animation_keep_anim_armature
|
export_settings['gltf_optimize_animation_keep_armature'] = self.export_optimize_animation_keep_anim_armature
|
||||||
export_settings['gltf_optimize_animation_keep_object'] = self.export_optimize_animation_keep_anim_object
|
export_settings['gltf_optimize_animation_keep_object'] = self.export_optimize_animation_keep_anim_object
|
||||||
|
export_settings['gltf_optimize_armature_disable_viewport'] = self.export_optimize_armature_disable_viewport
|
||||||
export_settings['gltf_export_anim_single_armature'] = self.export_anim_single_armature
|
export_settings['gltf_export_anim_single_armature'] = self.export_anim_single_armature
|
||||||
export_settings['gltf_export_reset_pose_bones'] = self.export_reset_pose_bones
|
export_settings['gltf_export_reset_pose_bones'] = self.export_reset_pose_bones
|
||||||
export_settings['gltf_export_reset_sk_data'] = self.export_morph_reset_sk_data
|
export_settings['gltf_export_reset_sk_data'] = self.export_morph_reset_sk_data
|
||||||
export_settings['gltf_bake_animation'] = self.export_bake_animation
|
export_settings['gltf_bake_animation'] = self.export_bake_animation
|
||||||
export_settings['gltf_negative_frames'] = self.export_negative_frame
|
export_settings['gltf_negative_frames'] = self.export_negative_frame
|
||||||
export_settings['gltf_anim_slide_to_zero'] = self.export_anim_slide_to_zero
|
export_settings['gltf_anim_slide_to_zero'] = self.export_anim_slide_to_zero
|
||||||
|
export_settings['gltf_export_extra_animations'] = self.export_extra_animations
|
||||||
else:
|
else:
|
||||||
export_settings['gltf_frame_range'] = False
|
export_settings['gltf_frame_range'] = False
|
||||||
export_settings['gltf_force_sampling'] = False
|
export_settings['gltf_force_sampling'] = False
|
||||||
@ -1028,9 +1068,11 @@ class ExportGLTF2_Base(ConvertGLTF2_Base):
|
|||||||
export_settings['gltf_optimize_animation'] = False
|
export_settings['gltf_optimize_animation'] = False
|
||||||
export_settings['gltf_optimize_animation_keep_armature'] = False
|
export_settings['gltf_optimize_animation_keep_armature'] = False
|
||||||
export_settings['gltf_optimize_animation_keep_object'] = False
|
export_settings['gltf_optimize_animation_keep_object'] = False
|
||||||
|
export_settings['gltf_optimize_armature_disable_viewport'] = False
|
||||||
export_settings['gltf_export_anim_single_armature'] = False
|
export_settings['gltf_export_anim_single_armature'] = False
|
||||||
export_settings['gltf_export_reset_pose_bones'] = False
|
export_settings['gltf_export_reset_pose_bones'] = False
|
||||||
export_settings['gltf_export_reset_sk_data'] = False
|
export_settings['gltf_export_reset_sk_data'] = False
|
||||||
|
export_settings['gltf_export_extra_animations'] = False
|
||||||
export_settings['gltf_skins'] = self.export_skins
|
export_settings['gltf_skins'] = self.export_skins
|
||||||
if self.export_skins:
|
if self.export_skins:
|
||||||
export_settings['gltf_all_vertex_influences'] = self.export_all_influences
|
export_settings['gltf_all_vertex_influences'] = self.export_all_influences
|
||||||
@ -1145,6 +1187,8 @@ class GLTF_PT_export_main(bpy.types.Panel):
|
|||||||
layout.prop(operator, 'export_keep_originals')
|
layout.prop(operator, 'export_keep_originals')
|
||||||
if operator.export_keep_originals is False:
|
if operator.export_keep_originals is False:
|
||||||
layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER')
|
layout.prop(operator, 'export_texture_dir', icon='FILE_FOLDER')
|
||||||
|
if operator.export_format == 'GLTF_EMBEDDED':
|
||||||
|
layout.label(text="This is the least efficient of the available forms, and should only be used when required.", icon='ERROR')
|
||||||
|
|
||||||
layout.prop(operator, 'export_copyright')
|
layout.prop(operator, 'export_copyright')
|
||||||
layout.prop(operator, 'will_save_settings')
|
layout.prop(operator, 'will_save_settings')
|
||||||
@ -1161,7 +1205,7 @@ class GLTF_PT_export_gltfpack(bpy.types.Panel):
|
|||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
gltfpack_path = context.preferences.addons['io_scene_gltf2'].preferences.gltfpack_path_ui.strip()
|
gltfpack_path = context.preferences.addons['io_scene_gltf2'].preferences.gltfpack_path_ui.strip()
|
||||||
if (gltfpack_path == ''): # gltfpack not setup in plugin preferences -> dont show any gltfpack relevant options in export dialog
|
if (gltfpack_path == ''): # gltfpack not setup in plugin preferences -> dont show any gltfpack relevant options in export dialog
|
||||||
return False;
|
return False
|
||||||
|
|
||||||
sfile = context.space_data
|
sfile = context.space_data
|
||||||
operator = sfile.active_operator
|
operator = sfile.active_operator
|
||||||
@ -1638,7 +1682,7 @@ class GLTF_PT_export_animation(bpy.types.Panel):
|
|||||||
layout.prop(operator, 'export_nla_strips_merged_animation_name')
|
layout.prop(operator, 'export_nla_strips_merged_animation_name')
|
||||||
|
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.active = operator.export_force_sampling and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS']
|
row.active = operator.export_force_sampling and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROACAST']
|
||||||
row.prop(operator, 'export_bake_animation')
|
row.prop(operator, 'export_bake_animation')
|
||||||
if operator.export_animation_mode == "SCENE":
|
if operator.export_animation_mode == "SCENE":
|
||||||
layout.prop(operator, 'export_anim_scene_split_object')
|
layout.prop(operator, 'export_anim_scene_split_object')
|
||||||
@ -1697,11 +1741,11 @@ class GLTF_PT_export_animation_ranges(bpy.types.Panel):
|
|||||||
|
|
||||||
layout.prop(operator, 'export_current_frame')
|
layout.prop(operator, 'export_current_frame')
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'NLA_TRACKS']
|
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROADCAST', 'NLA_TRACKS']
|
||||||
row.prop(operator, 'export_frame_range')
|
row.prop(operator, 'export_frame_range')
|
||||||
layout.prop(operator, 'export_anim_slide_to_zero')
|
layout.prop(operator, 'export_anim_slide_to_zero')
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'NLA_TRACKS']
|
row.active = operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROADCAST', 'NLA_TRACKS']
|
||||||
layout.prop(operator, 'export_negative_frame')
|
layout.prop(operator, 'export_negative_frame')
|
||||||
|
|
||||||
class GLTF_PT_export_animation_armature(bpy.types.Panel):
|
class GLTF_PT_export_animation_armature(bpy.types.Panel):
|
||||||
@ -1781,7 +1825,7 @@ class GLTF_PT_export_animation_sampling(bpy.types.Panel):
|
|||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
sfile = context.space_data
|
sfile = context.space_data
|
||||||
operator = sfile.active_operator
|
operator = sfile.active_operator
|
||||||
self.layout.active = operator.export_animations and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS']
|
self.layout.active = operator.export_animations and operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS', 'BROADCAST']
|
||||||
self.layout.prop(operator, "export_force_sampling", text="")
|
self.layout.prop(operator, "export_force_sampling", text="")
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
@ -1829,6 +1873,36 @@ class GLTF_PT_export_animation_optimize(bpy.types.Panel):
|
|||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.prop(operator, 'export_optimize_animation_keep_anim_object')
|
row.prop(operator, 'export_optimize_animation_keep_anim_object')
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(operator, 'export_optimize_armature_disable_viewport')
|
||||||
|
|
||||||
|
class GLTF_PT_export_animation_extra(bpy.types.Panel):
|
||||||
|
bl_space_type = 'FILE_BROWSER'
|
||||||
|
bl_region_type = 'TOOL_PROPS'
|
||||||
|
bl_label = "Extra Animations"
|
||||||
|
bl_parent_id = "GLTF_PT_export_animation"
|
||||||
|
bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
sfile = context.space_data
|
||||||
|
operator = sfile.active_operator
|
||||||
|
|
||||||
|
return operator.bl_idname == "EXPORT_SCENE_OT_gltf" and \
|
||||||
|
operator.export_animation_mode in ['ACTIONS', 'ACTIVE_ACTIONS']
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split = True
|
||||||
|
layout.use_property_decorate = False # No animation.
|
||||||
|
|
||||||
|
sfile = context.space_data
|
||||||
|
operator = sfile.active_operator
|
||||||
|
|
||||||
|
layout.active = operator.export_animations
|
||||||
|
|
||||||
|
layout.prop(operator, 'export_extra_animations')
|
||||||
|
|
||||||
|
|
||||||
class GLTF_PT_export_user_extensions(bpy.types.Panel):
|
class GLTF_PT_export_user_extensions(bpy.types.Panel):
|
||||||
bl_space_type = 'FILE_BROWSER'
|
bl_space_type = 'FILE_BROWSER'
|
||||||
@ -2112,6 +2186,12 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
|
|||||||
subtype='FILE_PATH'
|
subtype='FILE_PATH'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allow_embedded_format: bpy.props.BoolProperty(
|
||||||
|
default = False,
|
||||||
|
name='Allow glTF Embedded format',
|
||||||
|
description="Allow glTF Embedded format"
|
||||||
|
)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
@ -2120,6 +2200,10 @@ class GLTF_AddonPreferences(bpy.types.AddonPreferences):
|
|||||||
row.prop(self, "animation_ui", text="Animation UI")
|
row.prop(self, "animation_ui", text="Animation UI")
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.prop(self, "gltfpack_path_ui", text="Path to gltfpack")
|
row.prop(self, "gltfpack_path_ui", text="Path to gltfpack")
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(self, "allow_embedded_format", text="Allow glTF Embedded format")
|
||||||
|
if self.allow_embedded_format:
|
||||||
|
layout.label(text="This is the least efficient of the available forms, and should only be used when required.", icon='ERROR')
|
||||||
|
|
||||||
def menu_func_import(self, context):
|
def menu_func_import(self, context):
|
||||||
self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)')
|
self.layout.operator(ImportGLTF2.bl_idname, text='glTF 2.0 (.glb/.gltf)')
|
||||||
@ -2148,6 +2232,7 @@ classes = (
|
|||||||
GLTF_PT_export_animation_shapekeys,
|
GLTF_PT_export_animation_shapekeys,
|
||||||
GLTF_PT_export_animation_sampling,
|
GLTF_PT_export_animation_sampling,
|
||||||
GLTF_PT_export_animation_optimize,
|
GLTF_PT_export_animation_optimize,
|
||||||
|
GLTF_PT_export_animation_extra,
|
||||||
GLTF_PT_export_gltfpack,
|
GLTF_PT_export_gltfpack,
|
||||||
GLTF_PT_export_user_extensions,
|
GLTF_PT_export_user_extensions,
|
||||||
ImportGLTF2,
|
ImportGLTF2,
|
||||||
|
@ -5,11 +5,19 @@
|
|||||||
|
|
||||||
def get_target_property_name(data_path: str) -> str:
|
def get_target_property_name(data_path: str) -> str:
|
||||||
"""Retrieve target property."""
|
"""Retrieve target property."""
|
||||||
|
|
||||||
|
if data_path.endswith("]"):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
return data_path.rsplit('.', 1)[-1]
|
return data_path.rsplit('.', 1)[-1]
|
||||||
|
|
||||||
|
|
||||||
def get_target_object_path(data_path: str) -> str:
|
def get_target_object_path(data_path: str) -> str:
|
||||||
"""Retrieve target object data path without property"""
|
"""Retrieve target object data path without property"""
|
||||||
|
if data_path.endswith("]"):
|
||||||
|
return data_path.rsplit('[', 1)[0]
|
||||||
|
elif data_path.startswith("pose.bones["):
|
||||||
|
return data_path[:data_path.find('"]')] + '"]'
|
||||||
path_split = data_path.rsplit('.', 1)
|
path_split = data_path.rsplit('.', 1)
|
||||||
self_targeting = len(path_split) < 2
|
self_targeting = len(path_split) < 2
|
||||||
if self_targeting:
|
if self_targeting:
|
||||||
|
@ -564,7 +564,7 @@ class SCENE_PT_gltf2_action_filter(bpy.types.Panel):
|
|||||||
def poll(self, context):
|
def poll(self, context):
|
||||||
sfile = context.space_data
|
sfile = context.space_data
|
||||||
operator = sfile.active_operator
|
operator = sfile.active_operator
|
||||||
return operator.export_animation_mode in ["ACTIONS", "ACTIVE_ACTIONS"]
|
return operator.export_animation_mode in ["ACTIONS", "ACTIVE_ACTIONS", "BROADCAST"]
|
||||||
|
|
||||||
def draw_header(self, context):
|
def draw_header(self, context):
|
||||||
sfile = context.space_data
|
sfile = context.space_data
|
||||||
|
@ -16,7 +16,7 @@ def gather_animation_fcurves(
|
|||||||
|
|
||||||
name = __gather_name(blender_action, export_settings)
|
name = __gather_name(blender_action, export_settings)
|
||||||
|
|
||||||
channels, to_be_sampled = __gather_channels_fcurves(obj_uuid, blender_action, export_settings)
|
channels, to_be_sampled, extra_samplers = __gather_channels_fcurves(obj_uuid, blender_action, export_settings)
|
||||||
|
|
||||||
animation = gltf2_io.Animation(
|
animation = gltf2_io.Animation(
|
||||||
channels=channels,
|
channels=channels,
|
||||||
@ -27,12 +27,12 @@ def gather_animation_fcurves(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not animation.channels:
|
if not animation.channels:
|
||||||
return None, to_be_sampled
|
return None, to_be_sampled, extra_samplers
|
||||||
|
|
||||||
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
||||||
export_user_extensions('animation_gather_fcurve', export_settings, blender_object, blender_action)
|
export_user_extensions('animation_gather_fcurve', export_settings, blender_object, blender_action)
|
||||||
|
|
||||||
return animation, to_be_sampled
|
return animation, to_be_sampled, extra_samplers
|
||||||
|
|
||||||
def __gather_name(blender_action: bpy.types.Action,
|
def __gather_name(blender_action: bpy.types.Action,
|
||||||
export_settings
|
export_settings
|
||||||
|
@ -23,25 +23,40 @@ def gather_animation_fcurves_channels(
|
|||||||
export_settings
|
export_settings
|
||||||
):
|
):
|
||||||
|
|
||||||
channels_to_perform, to_be_sampled = get_channel_groups(obj_uuid, blender_action, export_settings)
|
channels_to_perform, to_be_sampled, extra_channels_to_perform = get_channel_groups(obj_uuid, blender_action, export_settings)
|
||||||
|
|
||||||
custom_range = None
|
custom_range = None
|
||||||
if blender_action.use_frame_range:
|
if blender_action.use_frame_range:
|
||||||
custom_range = (blender_action.frame_start, blender_action.frame_end)
|
custom_range = (blender_action.frame_start, blender_action.frame_end)
|
||||||
|
|
||||||
channels = []
|
channels = []
|
||||||
for chan in [chan for chan in channels_to_perform.values() if len(chan['properties']) != 0 and chan['type'] != "EXTRA"]:
|
extra_samplers = []
|
||||||
|
|
||||||
|
for chan in [chan for chan in channels_to_perform.values() if len(chan['properties']) != 0]:
|
||||||
for channel_group in chan['properties'].values():
|
for channel_group in chan['properties'].values():
|
||||||
channel = __gather_animation_fcurve_channel(chan['obj_uuid'], channel_group, chan['bone'], custom_range, export_settings)
|
channel = __gather_animation_fcurve_channel(chan['obj_uuid'], channel_group, chan['bone'], custom_range, export_settings)
|
||||||
if channel is not None:
|
if channel is not None:
|
||||||
channels.append(channel)
|
channels.append(channel)
|
||||||
|
|
||||||
|
|
||||||
return channels, to_be_sampled
|
if export_settings['gltf_export_extra_animations']:
|
||||||
|
for chan in [chan for chan in extra_channels_to_perform.values() if len(chan['properties']) != 0]:
|
||||||
|
for channel_group_name, channel_group in chan['properties'].items():
|
||||||
|
|
||||||
|
# No glTF channel here, as we don't have any target
|
||||||
|
# Trying to retrieve sampler directly
|
||||||
|
sampler = __gather_sampler(obj_uuid, tuple(channel_group), None, custom_range, True, export_settings)
|
||||||
|
if sampler is not None:
|
||||||
|
extra_samplers.append((channel_group_name, sampler, "OBJECT", None))
|
||||||
|
|
||||||
|
|
||||||
def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_settings):
|
return channels, to_be_sampled, extra_samplers
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_settings, no_sample_option=False):
|
||||||
|
# no_sample_option is used when we want to retrieve all SK channels, to be evaluate.
|
||||||
targets = {}
|
targets = {}
|
||||||
|
targets_extra = {}
|
||||||
|
|
||||||
|
|
||||||
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
||||||
@ -68,13 +83,26 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
|
|||||||
# find the object affected by this action
|
# find the object affected by this action
|
||||||
# object_path : blank for blender_object itself, key_blocks["<name>"] for SK, pose.bones["<name>"] for bones
|
# object_path : blank for blender_object itself, key_blocks["<name>"] for SK, pose.bones["<name>"] for bones
|
||||||
if not object_path:
|
if not object_path:
|
||||||
|
if fcurve.data_path.startswith("["):
|
||||||
|
target = blender_object
|
||||||
|
type_ = "EXTRA"
|
||||||
|
else:
|
||||||
target = blender_object
|
target = blender_object
|
||||||
type_ = "OBJECT"
|
type_ = "OBJECT"
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
target = get_object_from_datapath(blender_object, object_path)
|
target = get_object_from_datapath(blender_object, object_path)
|
||||||
|
|
||||||
if blender_object.type == "ARMATURE" and fcurve.data_path.startswith("pose.bones["):
|
if blender_object.type == "ARMATURE" and fcurve.data_path.startswith("pose.bones["):
|
||||||
|
if target_property is not None:
|
||||||
|
if get_target(target_property) is not None:
|
||||||
type_ = "BONE"
|
type_ = "BONE"
|
||||||
|
else:
|
||||||
|
type_ = "EXTRA"
|
||||||
|
else:
|
||||||
|
type_ = "EXTRA"
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
type_ = "EXTRA"
|
type_ = "EXTRA"
|
||||||
if blender_object.type == "MESH" and object_path.startswith("key_blocks"):
|
if blender_object.type == "MESH" and object_path.startswith("key_blocks"):
|
||||||
@ -108,6 +136,25 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
|
|||||||
multiple_rotation_mode_detected[target] = True
|
multiple_rotation_mode_detected[target] = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if type_ == "EXTRA":
|
||||||
|
# No group by property, because we are going to export fcurve separately
|
||||||
|
# We are going to evaluate fcurve, so no check if need to be sampled
|
||||||
|
if target_property is None:
|
||||||
|
target_property = fcurve.data_path
|
||||||
|
if not target_property.startswith("pose.bones["):
|
||||||
|
target_property = fcurve.data_path
|
||||||
|
target_data = targets_extra.get(target, {})
|
||||||
|
target_data['type'] = type_
|
||||||
|
target_data['bone'] = target.name
|
||||||
|
target_data['obj_uuid'] = obj_uuid
|
||||||
|
target_properties = target_data.get('properties', {})
|
||||||
|
channels = target_properties.get(target_property, [])
|
||||||
|
channels.append(fcurve)
|
||||||
|
target_properties[target_property] = channels
|
||||||
|
target_data['properties'] = target_properties
|
||||||
|
targets_extra[target] = target_data
|
||||||
|
continue
|
||||||
|
|
||||||
# group channels by target object and affected property of the target
|
# group channels by target object and affected property of the target
|
||||||
target_data = targets.get(target, {})
|
target_data = targets.get(target, {})
|
||||||
target_data['type'] = type_
|
target_data['type'] = type_
|
||||||
@ -148,7 +195,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
|
|||||||
# Check if the property can be exported without sampling
|
# Check if the property can be exported without sampling
|
||||||
new_properties = {}
|
new_properties = {}
|
||||||
for prop in target_data['properties'].keys():
|
for prop in target_data['properties'].keys():
|
||||||
if needs_baking(obj_uuid, target_data['properties'][prop], export_settings) is True:
|
if no_sample_option is False and needs_baking(obj_uuid, target_data['properties'][prop], export_settings) is True:
|
||||||
to_be_sampled.append((obj_uuid, target_data['type'], get_channel_from_target(get_target(prop)), target_data['bone'])) # bone can be None if not a bone :)
|
to_be_sampled.append((obj_uuid, target_data['type'], get_channel_from_target(get_target(prop)), target_data['bone'])) # bone can be None if not a bone :)
|
||||||
else:
|
else:
|
||||||
new_properties[prop] = target_data['properties'][prop]
|
new_properties[prop] = target_data['properties'][prop]
|
||||||
@ -165,7 +212,7 @@ def get_channel_groups(obj_uuid: str, blender_action: bpy.types.Action, export_s
|
|||||||
|
|
||||||
to_be_sampled = list(set(to_be_sampled))
|
to_be_sampled = list(set(to_be_sampled))
|
||||||
|
|
||||||
return targets, to_be_sampled
|
return targets, to_be_sampled, targets_extra
|
||||||
|
|
||||||
|
|
||||||
def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object):
|
def __get_channel_group_sorted(channels: typing.Tuple[bpy.types.FCurve], blender_object: bpy.types.Object):
|
||||||
@ -226,7 +273,7 @@ def __gather_animation_fcurve_channel(obj_uuid: str,
|
|||||||
|
|
||||||
__target= __gather_target(obj_uuid, channel_group, bone, export_settings)
|
__target= __gather_target(obj_uuid, channel_group, bone, export_settings)
|
||||||
if __target.path is not None:
|
if __target.path is not None:
|
||||||
sampler = __gather_sampler(obj_uuid, channel_group, bone, custom_range, export_settings)
|
sampler = __gather_sampler(obj_uuid, channel_group, bone, custom_range, False, export_settings)
|
||||||
|
|
||||||
if sampler is None:
|
if sampler is None:
|
||||||
# After check, no need to animate this node for this channel
|
# After check, no need to animate this node for this channel
|
||||||
@ -261,9 +308,10 @@ def __gather_sampler(obj_uuid: str,
|
|||||||
channel_group: typing.Tuple[bpy.types.FCurve],
|
channel_group: typing.Tuple[bpy.types.FCurve],
|
||||||
bone: typing.Optional[str],
|
bone: typing.Optional[str],
|
||||||
custom_range: typing.Optional[set],
|
custom_range: typing.Optional[set],
|
||||||
|
extra_mode: bool,
|
||||||
export_settings) -> gltf2_io.AnimationSampler:
|
export_settings) -> gltf2_io.AnimationSampler:
|
||||||
|
|
||||||
return gather_animation_fcurves_sampler(obj_uuid, channel_group, bone, custom_range, export_settings)
|
return gather_animation_fcurves_sampler(obj_uuid, channel_group, bone, custom_range, extra_mode, export_settings)
|
||||||
|
|
||||||
def needs_baking(obj_uuid: str,
|
def needs_baking(obj_uuid: str,
|
||||||
channels: typing.Tuple[bpy.types.FCurve],
|
channels: typing.Tuple[bpy.types.FCurve],
|
||||||
|
@ -16,11 +16,12 @@ def gather_fcurve_keyframes(
|
|||||||
channel_group: typing.Tuple[bpy.types.FCurve],
|
channel_group: typing.Tuple[bpy.types.FCurve],
|
||||||
bone: typing.Optional[str],
|
bone: typing.Optional[str],
|
||||||
custom_range: typing.Optional[set],
|
custom_range: typing.Optional[set],
|
||||||
|
extra_mode: bool,
|
||||||
export_settings):
|
export_settings):
|
||||||
|
|
||||||
keyframes = []
|
keyframes = []
|
||||||
|
|
||||||
non_keyed_values = __gather_non_keyed_values(obj_uuid, channel_group, bone, export_settings)
|
non_keyed_values = gather_non_keyed_values(obj_uuid, channel_group, bone, extra_mode, export_settings)
|
||||||
|
|
||||||
# Just use the keyframes as they are specified in blender
|
# Just use the keyframes as they are specified in blender
|
||||||
# Note: channels has some None items only for SK if some SK are not animated
|
# Note: channels has some None items only for SK if some SK are not animated
|
||||||
@ -45,7 +46,7 @@ def gather_fcurve_keyframes(
|
|||||||
key.value = [c.evaluate(frame) for c in channel_group if c is not None]
|
key.value = [c.evaluate(frame) for c in channel_group if c is not None]
|
||||||
# Complete key with non keyed values, if needed
|
# Complete key with non keyed values, if needed
|
||||||
if len([c for c in channel_group if c is not None]) != key.get_target_len():
|
if len([c for c in channel_group if c is not None]) != key.get_target_len():
|
||||||
__complete_key(key, non_keyed_values)
|
complete_key(key, non_keyed_values)
|
||||||
|
|
||||||
# compute tangents for cubic spline interpolation
|
# compute tangents for cubic spline interpolation
|
||||||
if [c for c in channel_group if c is not None][0].keyframe_points[0].interpolation == "BEZIER":
|
if [c for c in channel_group if c is not None][0].keyframe_points[0].interpolation == "BEZIER":
|
||||||
@ -87,13 +88,18 @@ def gather_fcurve_keyframes(
|
|||||||
return keyframes
|
return keyframes
|
||||||
|
|
||||||
|
|
||||||
def __gather_non_keyed_values(
|
def gather_non_keyed_values(
|
||||||
obj_uuid: str,
|
obj_uuid: str,
|
||||||
channel_group: typing.Tuple[bpy.types.FCurve],
|
channel_group: typing.Tuple[bpy.types.FCurve],
|
||||||
bone: typing.Optional[str],
|
bone: typing.Optional[str],
|
||||||
|
extra_mode: bool,
|
||||||
export_settings
|
export_settings
|
||||||
) -> typing.Tuple[typing.Optional[float]]:
|
) -> typing.Tuple[typing.Optional[float]]:
|
||||||
|
|
||||||
|
if extra_mode is True:
|
||||||
|
# No need to check if there are non non keyed values, as we export fcurve independently
|
||||||
|
return [None]
|
||||||
|
|
||||||
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
||||||
|
|
||||||
non_keyed_values = []
|
non_keyed_values = []
|
||||||
@ -132,7 +138,7 @@ def __gather_non_keyed_values(
|
|||||||
if i in indices:
|
if i in indices:
|
||||||
non_keyed_values.append(None)
|
non_keyed_values.append(None)
|
||||||
else:
|
else:
|
||||||
if bone is None is None:
|
if bone is None:
|
||||||
non_keyed_values.append({
|
non_keyed_values.append({
|
||||||
"delta_location" : blender_object.delta_location,
|
"delta_location" : blender_object.delta_location,
|
||||||
"delta_rotation_euler" : blender_object.delta_rotation_euler,
|
"delta_rotation_euler" : blender_object.delta_rotation_euler,
|
||||||
@ -178,7 +184,7 @@ def __gather_non_keyed_values(
|
|||||||
return tuple(non_keyed_values)
|
return tuple(non_keyed_values)
|
||||||
|
|
||||||
|
|
||||||
def __complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[float]]):
|
def complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[float]]):
|
||||||
"""
|
"""
|
||||||
Complete keyframe with non keyed values
|
Complete keyframe with non keyed values
|
||||||
"""
|
"""
|
||||||
|
@ -23,6 +23,7 @@ def gather_animation_fcurves_sampler(
|
|||||||
channel_group: typing.Tuple[bpy.types.FCurve],
|
channel_group: typing.Tuple[bpy.types.FCurve],
|
||||||
bone: typing.Optional[str],
|
bone: typing.Optional[str],
|
||||||
custom_range: typing.Optional[set],
|
custom_range: typing.Optional[set],
|
||||||
|
extra_mode: bool,
|
||||||
export_settings
|
export_settings
|
||||||
) -> gltf2_io.AnimationSampler:
|
) -> gltf2_io.AnimationSampler:
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ def gather_animation_fcurves_sampler(
|
|||||||
channel_group,
|
channel_group,
|
||||||
bone,
|
bone,
|
||||||
custom_range,
|
custom_range,
|
||||||
|
extra_mode,
|
||||||
export_settings)
|
export_settings)
|
||||||
|
|
||||||
if keyframes is None:
|
if keyframes is None:
|
||||||
@ -40,7 +42,7 @@ def gather_animation_fcurves_sampler(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Now we are raw input/output, we need to convert to glTF data
|
# Now we are raw input/output, we need to convert to glTF data
|
||||||
input, output = __convert_keyframes(obj_uuid, channel_group, bone, keyframes, export_settings)
|
input, output = __convert_keyframes(obj_uuid, channel_group, bone, keyframes, extra_mode, export_settings)
|
||||||
|
|
||||||
sampler = gltf2_io.AnimationSampler(
|
sampler = gltf2_io.AnimationSampler(
|
||||||
extensions=None,
|
extensions=None,
|
||||||
@ -62,16 +64,18 @@ def __gather_keyframes(
|
|||||||
channel_group: typing.Tuple[bpy.types.FCurve],
|
channel_group: typing.Tuple[bpy.types.FCurve],
|
||||||
bone: typing.Optional[str],
|
bone: typing.Optional[str],
|
||||||
custom_range: typing.Optional[set],
|
custom_range: typing.Optional[set],
|
||||||
|
extra_mode: bool,
|
||||||
export_settings
|
export_settings
|
||||||
):
|
):
|
||||||
|
|
||||||
return gather_fcurve_keyframes(obj_uuid, channel_group, bone, custom_range, export_settings)
|
return gather_fcurve_keyframes(obj_uuid, channel_group, bone, custom_range, extra_mode, export_settings)
|
||||||
|
|
||||||
def __convert_keyframes(
|
def __convert_keyframes(
|
||||||
obj_uuid: str,
|
obj_uuid: str,
|
||||||
channel_group: typing.Tuple[bpy.types.FCurve],
|
channel_group: typing.Tuple[bpy.types.FCurve],
|
||||||
bone_name: typing.Optional[str],
|
bone_name: typing.Optional[str],
|
||||||
keyframes,
|
keyframes,
|
||||||
|
extra_mode: bool,
|
||||||
export_settings):
|
export_settings):
|
||||||
|
|
||||||
times = [k.seconds for k in keyframes]
|
times = [k.seconds for k in keyframes]
|
||||||
@ -137,6 +141,17 @@ def __convert_keyframes(
|
|||||||
values = []
|
values = []
|
||||||
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
fps = (bpy.context.scene.render.fps * bpy.context.scene.render.fps_base)
|
||||||
for keyframe in keyframes:
|
for keyframe in keyframes:
|
||||||
|
|
||||||
|
if extra_mode is True:
|
||||||
|
# Export as is, without trying to convert
|
||||||
|
keyframe_value = keyframe.value
|
||||||
|
if keyframe.in_tangent is not None:
|
||||||
|
keyframe_value = keyframe.in_tangent + keyframe_value
|
||||||
|
if keyframe.out_tangent is not None:
|
||||||
|
keyframe_value = keyframe_value + keyframe.out_tangent
|
||||||
|
values += keyframe_value
|
||||||
|
continue
|
||||||
|
|
||||||
# Transform the data and build gltf control points
|
# Transform the data and build gltf control points
|
||||||
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction)
|
value = gltf2_blender_math.transform(keyframe.value, target_datapath, transform, need_rotation_correction)
|
||||||
if is_yup and bone_name is None:
|
if is_yup and bone_name is None:
|
||||||
|
@ -261,6 +261,25 @@ def gather_action_animations( obj_uuid: int,
|
|||||||
current_use_nla = blender_object.animation_data.use_nla
|
current_use_nla = blender_object.animation_data.use_nla
|
||||||
blender_object.animation_data.use_nla = False
|
blender_object.animation_data.use_nla = False
|
||||||
|
|
||||||
|
# Try to disable all except armature in viewport, for performance
|
||||||
|
if export_settings['gltf_optimize_armature_disable_viewport'] \
|
||||||
|
and export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE":
|
||||||
|
|
||||||
|
# If the skinned mesh has driver(s), we can't disable it to bake armature.
|
||||||
|
need_to_enable_again = False
|
||||||
|
sk_drivers = get_sk_drivers(obj_uuid, export_settings)
|
||||||
|
if len(sk_drivers) == 0:
|
||||||
|
need_to_enable_again = True
|
||||||
|
# Before baking, disabling from viewport all meshes
|
||||||
|
for obj in [n.blender_object for n in export_settings['vtree'].nodes.values() if n.blender_type in
|
||||||
|
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
|
||||||
|
obj.hide_viewport = True
|
||||||
|
export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = False
|
||||||
|
else:
|
||||||
|
print_console("WARNING", "Can't disable viewport because of drivers")
|
||||||
|
export_settings['gltf_optimize_armature_disable_viewport'] = False # We changed the option here, so we don't need to re-check it later, during
|
||||||
|
|
||||||
|
|
||||||
export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, False)
|
export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, False)
|
||||||
|
|
||||||
######## Export
|
######## Export
|
||||||
@ -296,9 +315,9 @@ def gather_action_animations( obj_uuid: int,
|
|||||||
|
|
||||||
if export_settings['gltf_force_sampling'] is True:
|
if export_settings['gltf_force_sampling'] is True:
|
||||||
if export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE":
|
if export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE":
|
||||||
animation = gather_action_armature_sampled(obj_uuid, blender_action, None, export_settings)
|
animation, extra_samplers = gather_action_armature_sampled(obj_uuid, blender_action, None, export_settings)
|
||||||
elif on_type == "OBJECT":
|
elif on_type == "OBJECT":
|
||||||
animation = gather_action_object_sampled(obj_uuid, blender_action, None, export_settings)
|
animation, extra_samplers = gather_action_object_sampled(obj_uuid, blender_action, None, export_settings)
|
||||||
else:
|
else:
|
||||||
animation = gather_action_sk_sampled(obj_uuid, blender_action, None, export_settings)
|
animation = gather_action_sk_sampled(obj_uuid, blender_action, None, export_settings)
|
||||||
else:
|
else:
|
||||||
@ -307,7 +326,7 @@ def gather_action_animations( obj_uuid: int,
|
|||||||
# - animation on fcurves
|
# - animation on fcurves
|
||||||
# - fcurve that cannot be handled not sampled, to be sampled
|
# - fcurve that cannot be handled not sampled, to be sampled
|
||||||
# to_be_sampled is : (object_uuid , type , prop, optional(bone.name) )
|
# to_be_sampled is : (object_uuid , type , prop, optional(bone.name) )
|
||||||
animation, to_be_sampled = gather_animation_fcurves(obj_uuid, blender_action, export_settings)
|
animation, to_be_sampled, extra_samplers = gather_animation_fcurves(obj_uuid, blender_action, export_settings)
|
||||||
for (obj_uuid, type_, prop, bone) in to_be_sampled:
|
for (obj_uuid, type_, prop, bone) in to_be_sampled:
|
||||||
if type_ == "BONE":
|
if type_ == "BONE":
|
||||||
channel = gather_sampled_bone_channel(obj_uuid, bone, prop, blender_action.name, True, get_gltf_interpolation("LINEAR"), export_settings)
|
channel = gather_sampled_bone_channel(obj_uuid, bone, prop, blender_action.name, True, get_gltf_interpolation("LINEAR"), export_settings)
|
||||||
@ -334,6 +353,11 @@ def gather_action_animations( obj_uuid: int,
|
|||||||
if channel is not None:
|
if channel is not None:
|
||||||
animation.channels.append(channel)
|
animation.channels.append(channel)
|
||||||
|
|
||||||
|
# Add extra samplers
|
||||||
|
# Because this is not core glTF specification, you can add extra samplers using hook
|
||||||
|
if export_settings['gltf_export_extra_animations'] and len(extra_samplers) != 0:
|
||||||
|
export_user_extensions('extra_animation_manage', export_settings, extra_samplers, obj_uuid, blender_object, blender_action, animation)
|
||||||
|
|
||||||
# If we are in a SK animation, and we need to bake (if there also in TRS anim)
|
# If we are in a SK animation, and we need to bake (if there also in TRS anim)
|
||||||
if len([a for a in blender_actions if a[2] == "OBJECT"]) == 0 and on_type == "SHAPEKEY":
|
if len([a for a in blender_actions if a[2] == "OBJECT"]) == 0 and on_type == "SHAPEKEY":
|
||||||
if export_settings['gltf_bake_animation'] is True and export_settings['gltf_force_sampling'] is True:
|
if export_settings['gltf_bake_animation'] is True and export_settings['gltf_force_sampling'] is True:
|
||||||
@ -343,7 +367,7 @@ def gather_action_animations( obj_uuid: int,
|
|||||||
if obj_uuid not in export_settings['ranges'].keys():
|
if obj_uuid not in export_settings['ranges'].keys():
|
||||||
export_settings['ranges'][obj_uuid] = {}
|
export_settings['ranges'][obj_uuid] = {}
|
||||||
export_settings['ranges'][obj_uuid][obj_uuid] = export_settings['ranges'][obj_uuid][blender_action.name]
|
export_settings['ranges'][obj_uuid][obj_uuid] = export_settings['ranges'][obj_uuid][blender_action.name]
|
||||||
channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
channels, _ = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
||||||
if channels is not None:
|
if channels is not None:
|
||||||
if animation is None:
|
if animation is None:
|
||||||
animation = gltf2_io.Animation(
|
animation = gltf2_io.Animation(
|
||||||
@ -431,6 +455,15 @@ def gather_action_animations( obj_uuid: int,
|
|||||||
if blender_object and current_world_matrix is not None:
|
if blender_object and current_world_matrix is not None:
|
||||||
blender_object.matrix_world = current_world_matrix
|
blender_object.matrix_world = current_world_matrix
|
||||||
|
|
||||||
|
if export_settings['gltf_optimize_armature_disable_viewport'] \
|
||||||
|
and export_settings['vtree'].nodes[obj_uuid].blender_object.type == "ARMATURE":
|
||||||
|
if need_to_enable_again is True:
|
||||||
|
# And now, restoring meshes in viewport
|
||||||
|
for node, obj in [(n, n.blender_object) for n in export_settings['vtree'].nodes.values() if n.blender_type in
|
||||||
|
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
|
||||||
|
obj.hide_viewport = node.default_hide_viewport
|
||||||
|
export_settings['vtree'].nodes[obj_uuid].blender_object.hide_viewport = export_settings['vtree'].nodes[obj_uuid].default_hide_viewport
|
||||||
|
|
||||||
export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, True)
|
export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, True)
|
||||||
|
|
||||||
return animations, tracks
|
return animations, tracks
|
||||||
@ -447,6 +480,9 @@ def __get_blender_actions(obj_uuid: str,
|
|||||||
|
|
||||||
export_user_extensions('pre_gather_actions_hook', export_settings, blender_object)
|
export_user_extensions('pre_gather_actions_hook', export_settings, blender_object)
|
||||||
|
|
||||||
|
if export_settings['gltf_animation_mode'] == "BROADCAST":
|
||||||
|
return __get_blender_actions_broadcast(obj_uuid, export_settings)
|
||||||
|
|
||||||
if blender_object and blender_object.animation_data is not None:
|
if blender_object and blender_object.animation_data is not None:
|
||||||
# Collect active action.
|
# Collect active action.
|
||||||
if blender_object.animation_data.action is not None:
|
if blender_object.animation_data.action is not None:
|
||||||
@ -571,3 +607,72 @@ def __gather_extras(blender_action, export_settings):
|
|||||||
if export_settings['gltf_extras']:
|
if export_settings['gltf_extras']:
|
||||||
return generate_extras(blender_action)
|
return generate_extras(blender_action)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def __get_blender_actions_broadcast(obj_uuid, export_settings):
|
||||||
|
blender_actions = []
|
||||||
|
blender_tracks = {}
|
||||||
|
action_on_type = {}
|
||||||
|
|
||||||
|
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
||||||
|
|
||||||
|
# Note : Like in FBX exporter:
|
||||||
|
# - Object with animation data will get all actions
|
||||||
|
# - Object without animation will not get any action
|
||||||
|
|
||||||
|
# Collect all actions
|
||||||
|
for blender_action in bpy.data.actions:
|
||||||
|
if hasattr(bpy.data.scenes[0], "gltf_action_filter") \
|
||||||
|
and id(blender_action) in [id(item.action) for item in bpy.data.scenes[0].gltf_action_filter if item.keep is False]:
|
||||||
|
continue # We ignore this action
|
||||||
|
|
||||||
|
# Keep all actions on objects (no Shapekey animation, No armature animation (on bones))
|
||||||
|
if blender_action.id_root == "OBJECT": #TRS and Bone animations
|
||||||
|
if blender_object.animation_data is None:
|
||||||
|
continue
|
||||||
|
if blender_object and blender_object.type == "ARMATURE" and __is_armature_action(blender_action):
|
||||||
|
blender_actions.append(blender_action)
|
||||||
|
blender_tracks[blender_action.name] = None
|
||||||
|
action_on_type[blender_action.name] = "OBJECT"
|
||||||
|
elif blender_object.type == "MESH":
|
||||||
|
if not __is_armature_action(blender_action):
|
||||||
|
blender_actions.append(blender_action)
|
||||||
|
blender_tracks[blender_action.name] = None
|
||||||
|
action_on_type[blender_action.name] = "OBJECT"
|
||||||
|
elif blender_action.id_root == "KEY":
|
||||||
|
if blender_object.type != "MESH" or blender_object.data is None or blender_object.data.shape_keys is None or blender_object.data.shape_keys.animation_data is None:
|
||||||
|
continue
|
||||||
|
# Checking that the object has some SK and some animation on it
|
||||||
|
if blender_object is None:
|
||||||
|
continue
|
||||||
|
if blender_object.type != "MESH":
|
||||||
|
continue
|
||||||
|
if blender_object.data is None or blender_object.data.shape_keys is None:
|
||||||
|
continue
|
||||||
|
blender_actions.append(blender_action)
|
||||||
|
blender_tracks[blender_action.name] = None
|
||||||
|
action_on_type[blender_action.name] = "SHAPEKEY"
|
||||||
|
|
||||||
|
|
||||||
|
# Use a class to get parameters, to be able to modify them
|
||||||
|
class GatherActionHookParameters:
|
||||||
|
def __init__(self, blender_actions, blender_tracks, action_on_type):
|
||||||
|
self.blender_actions = blender_actions
|
||||||
|
self.blender_tracks = blender_tracks
|
||||||
|
self.action_on_type = action_on_type
|
||||||
|
|
||||||
|
gatheractionhookparams = GatherActionHookParameters(blender_actions, blender_tracks, action_on_type)
|
||||||
|
|
||||||
|
export_user_extensions('gather_actions_hook', export_settings, blender_object, gatheractionhookparams)
|
||||||
|
|
||||||
|
# Get params back from hooks
|
||||||
|
blender_actions = gatheractionhookparams.blender_actions
|
||||||
|
blender_tracks = gatheractionhookparams.blender_tracks
|
||||||
|
action_on_type = gatheractionhookparams.action_on_type
|
||||||
|
|
||||||
|
# Remove duplicate actions.
|
||||||
|
blender_actions = list(set(blender_actions))
|
||||||
|
# sort animations alphabetically (case insensitive) so they have a defined order and match Blender's Action list
|
||||||
|
blender_actions.sort(key = lambda a: a.name.lower())
|
||||||
|
|
||||||
|
return [(blender_action, blender_tracks[blender_action.name], action_on_type[blender_action.name]) for blender_action in blender_actions]
|
||||||
|
|
||||||
|
@ -162,6 +162,9 @@ def merge_tracks_perform(merged_tracks, animations, export_settings):
|
|||||||
|
|
||||||
def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None):
|
def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None):
|
||||||
|
|
||||||
|
# Bake situation does not export any extra animation channels, as we bake TRS + weights on Track or scene level, without direct
|
||||||
|
# Access to fcurve and action data
|
||||||
|
|
||||||
# if there is no animation in file => no need to bake
|
# if there is no animation in file => no need to bake
|
||||||
if len(bpy.data.actions) == 0:
|
if len(bpy.data.actions) == 0:
|
||||||
return None
|
return None
|
||||||
@ -180,8 +183,7 @@ def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None
|
|||||||
# (skinned meshes TRS must be ignored, says glTF specification)
|
# (skinned meshes TRS must be ignored, says glTF specification)
|
||||||
if export_settings['vtree'].nodes[obj_uuid].skin is None:
|
if export_settings['vtree'].nodes[obj_uuid].skin is None:
|
||||||
if mode is None or mode == "OBJECT":
|
if mode is None or mode == "OBJECT":
|
||||||
animation = gather_action_object_sampled(obj_uuid, None, animation_key, export_settings)
|
animation, _ = gather_action_object_sampled(obj_uuid, None, animation_key, export_settings)
|
||||||
|
|
||||||
|
|
||||||
# Need to bake sk only if not linked to a driver sk by parent armature
|
# Need to bake sk only if not linked to a driver sk by parent armature
|
||||||
# In case of NLA track export, no baking of SK
|
# In case of NLA track export, no baking of SK
|
||||||
@ -227,7 +229,7 @@ def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None
|
|||||||
# We need to bake all bones. Because some bone can have some constraints linking to
|
# We need to bake all bones. Because some bone can have some constraints linking to
|
||||||
# some other armature bones, for example
|
# some other armature bones, for example
|
||||||
|
|
||||||
animation = gather_action_armature_sampled(obj_uuid, None, animation_key, export_settings)
|
animation, _ = gather_action_armature_sampled(obj_uuid, None, animation_key, export_settings)
|
||||||
link_samplers(animation, export_settings)
|
link_samplers(animation, export_settings)
|
||||||
if animation is not None:
|
if animation is not None:
|
||||||
return animation
|
return animation
|
||||||
|
@ -14,7 +14,7 @@ def gather_animations(export_settings):
|
|||||||
export_settings['ranges'] = {}
|
export_settings['ranges'] = {}
|
||||||
export_settings['slide'] = {}
|
export_settings['slide'] = {}
|
||||||
|
|
||||||
if export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
if export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
|
||||||
return gather_actions_animations(export_settings)
|
return gather_actions_animations(export_settings)
|
||||||
elif export_settings['gltf_animation_mode'] == "SCENE":
|
elif export_settings['gltf_animation_mode'] == "SCENE":
|
||||||
return gather_scene_animations(export_settings)
|
return gather_scene_animations(export_settings)
|
||||||
|
@ -15,12 +15,21 @@ class Keyframe:
|
|||||||
self.__length_morph = 0
|
self.__length_morph = 0
|
||||||
# Note: channels has some None items only for SK if some SK are not animated
|
# Note: channels has some None items only for SK if some SK are not animated
|
||||||
if bake_channel is None:
|
if bake_channel is None:
|
||||||
|
if not all([c == None for c in channels]):
|
||||||
self.target = [c for c in channels if c is not None][0].data_path.split('.')[-1]
|
self.target = [c for c in channels if c is not None][0].data_path.split('.')[-1]
|
||||||
if self.target != "value":
|
if self.target != "value":
|
||||||
self.__indices = [c.array_index for c in channels]
|
self.__indices = [c.array_index for c in channels]
|
||||||
else:
|
else:
|
||||||
self.__indices = [i for i, c in enumerate(channels) if c is not None]
|
self.__indices = [i for i, c in enumerate(channels) if c is not None]
|
||||||
self.__length_morph = len(channels)
|
self.__length_morph = len(channels)
|
||||||
|
else:
|
||||||
|
# If all channels are None (baking evaluate SK case)
|
||||||
|
self.target = "value"
|
||||||
|
self.__indices = []
|
||||||
|
self.__length_morph = len(channels)
|
||||||
|
for i in range(self.get_target_len()):
|
||||||
|
self.__indices.append(i)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if bake_channel == "value":
|
if bake_channel == "value":
|
||||||
self.__length_morph = len(channels)
|
self.__length_morph = len(channels)
|
||||||
@ -47,10 +56,7 @@ class Keyframe:
|
|||||||
"rotation_quaternion": 4,
|
"rotation_quaternion": 4,
|
||||||
"scale": 3,
|
"scale": 3,
|
||||||
"value": self.__length_morph
|
"value": self.__length_morph
|
||||||
}.get(self.target)
|
}.get(self.target, 1)
|
||||||
|
|
||||||
if length is None:
|
|
||||||
raise RuntimeError("Animations with target type '{}' are not supported.".format(self.target))
|
|
||||||
|
|
||||||
return length
|
return length
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ def gather_scene_animations(export_settings):
|
|||||||
if blender_object and blender_object.type != "ARMATURE":
|
if blender_object and blender_object.type != "ARMATURE":
|
||||||
# We have to check if this is a skinned mesh, because we don't have to force animation baking on this case
|
# We have to check if this is a skinned mesh, because we don't have to force animation baking on this case
|
||||||
if export_settings['vtree'].nodes[obj_uuid].skin is None:
|
if export_settings['vtree'].nodes[obj_uuid].skin is None:
|
||||||
channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
channels, _ = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
||||||
if channels is not None:
|
if channels is not None:
|
||||||
total_channels.extend(channels)
|
total_channels.extend(channels)
|
||||||
if export_settings['gltf_morph_anim'] and blender_object.type == "MESH" \
|
if export_settings['gltf_morph_anim'] and blender_object.type == "MESH" \
|
||||||
@ -90,11 +90,11 @@ def gather_scene_animations(export_settings):
|
|||||||
elif blender_object is None:
|
elif blender_object is None:
|
||||||
# This is GN instances
|
# This is GN instances
|
||||||
# Currently, not checking if this instance is skinned.... #TODO
|
# Currently, not checking if this instance is skinned.... #TODO
|
||||||
channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
channels, _ = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
||||||
if channels is not None:
|
if channels is not None:
|
||||||
total_channels.extend(channels)
|
total_channels.extend(channels)
|
||||||
else:
|
else:
|
||||||
channels = gather_armature_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
channels, _ = gather_armature_sampled_channels(obj_uuid, obj_uuid, export_settings)
|
||||||
if channels is not None:
|
if channels is not None:
|
||||||
total_channels.extend(channels)
|
total_channels.extend(channels)
|
||||||
|
|
||||||
|
@ -47,6 +47,9 @@ def gather_track_animations( obj_uuid: int,
|
|||||||
|
|
||||||
animations = []
|
animations = []
|
||||||
|
|
||||||
|
# Bake situation does not export any extra animation channels, as we bake TRS + weights on Track or scene level, without direct
|
||||||
|
# Access to fcurve and action data
|
||||||
|
|
||||||
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object
|
||||||
# Collect all tracks affecting this object.
|
# Collect all tracks affecting this object.
|
||||||
blender_tracks = __get_blender_tracks(obj_uuid, export_settings)
|
blender_tracks = __get_blender_tracks(obj_uuid, export_settings)
|
||||||
|
@ -8,6 +8,7 @@ from ......io.exp.gltf2_io_user_extensions import export_user_extensions
|
|||||||
from ......io.com.gltf2_io_debug import print_console
|
from ......io.com.gltf2_io_debug import print_console
|
||||||
from ......io.com import gltf2_io
|
from ......io.com import gltf2_io
|
||||||
from .....com.gltf2_blender_extras import generate_extras
|
from .....com.gltf2_blender_extras import generate_extras
|
||||||
|
from ...fcurves.gltf2_blender_gather_fcurves_sampler import gather_animation_fcurves_sampler
|
||||||
from .armature_channels import gather_armature_sampled_channels
|
from .armature_channels import gather_armature_sampled_channels
|
||||||
|
|
||||||
|
|
||||||
@ -22,8 +23,9 @@ def gather_action_armature_sampled(armature_uuid: str, blender_action: typing.Op
|
|||||||
name = __gather_name(blender_action, armature_uuid, cache_key, export_settings)
|
name = __gather_name(blender_action, armature_uuid, cache_key, export_settings)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
channels, extra_channels = __gather_channels(armature_uuid, blender_action.name if blender_action else cache_key, export_settings)
|
||||||
animation = gltf2_io.Animation(
|
animation = gltf2_io.Animation(
|
||||||
channels=__gather_channels(armature_uuid, blender_action.name if blender_action else cache_key, export_settings),
|
channels=channels,
|
||||||
extensions=None,
|
extensions=None,
|
||||||
extras=__gather_extras(blender_action, export_settings),
|
extras=__gather_extras(blender_action, export_settings),
|
||||||
name=name,
|
name=name,
|
||||||
@ -35,15 +37,28 @@ def gather_action_armature_sampled(armature_uuid: str, blender_action: typing.Op
|
|||||||
|
|
||||||
export_user_extensions('pre_gather_animation_hook', export_settings, animation, blender_action, blender_object)
|
export_user_extensions('pre_gather_animation_hook', export_settings, animation, blender_action, blender_object)
|
||||||
|
|
||||||
|
|
||||||
|
extra_samplers = []
|
||||||
|
if export_settings['gltf_export_extra_animations']:
|
||||||
|
for chan in [chan for chan in extra_channels.values() if len(chan['properties']) != 0]:
|
||||||
|
for channel_group_name, channel_group in chan['properties'].items():
|
||||||
|
|
||||||
|
# No glTF channel here, as we don't have any target
|
||||||
|
# Trying to retrieve sampler directly
|
||||||
|
sampler = gather_animation_fcurves_sampler(armature_uuid, tuple(channel_group), None, None, True, export_settings)
|
||||||
|
if sampler is not None:
|
||||||
|
extra_samplers.append((channel_group_name, sampler))
|
||||||
|
|
||||||
|
|
||||||
if not animation.channels:
|
if not animation.channels:
|
||||||
return None
|
return None, extra_samplers
|
||||||
|
|
||||||
# To allow reuse of samplers in one animation : This will be done later, when we know all channels are here
|
# To allow reuse of samplers in one animation : This will be done later, when we know all channels are here
|
||||||
|
|
||||||
export_user_extensions('gather_animation_hook', export_settings, animation, blender_action, blender_object) # For compatibility for older version
|
export_user_extensions('gather_animation_hook', export_settings, animation, blender_action, blender_object) # For compatibility for older version
|
||||||
export_user_extensions('animation_action_armature_sampled', export_settings, animation, blender_object, blender_action, cache_key)
|
export_user_extensions('animation_action_armature_sampled', export_settings, animation, blender_object, blender_action, cache_key)
|
||||||
|
|
||||||
return animation
|
return animation, extra_samplers
|
||||||
|
|
||||||
def __gather_name(blender_action: bpy.types.Action,
|
def __gather_name(blender_action: bpy.types.Action,
|
||||||
armature_uuid: str,
|
armature_uuid: str,
|
||||||
|
@ -18,6 +18,7 @@ from .armature_sampler import gather_bone_sampled_animation_sampler
|
|||||||
|
|
||||||
def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_settings) -> typing.List[gltf2_io.AnimationChannel]:
|
def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_settings) -> typing.List[gltf2_io.AnimationChannel]:
|
||||||
channels = []
|
channels = []
|
||||||
|
extra_channels = {}
|
||||||
|
|
||||||
# Then bake all bones
|
# Then bake all bones
|
||||||
bones_to_be_animated = []
|
bones_to_be_animated = []
|
||||||
@ -28,7 +29,7 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
|
|||||||
list_of_animated_bone_channels = {}
|
list_of_animated_bone_channels = {}
|
||||||
if armature_uuid != blender_action_name and blender_action_name in bpy.data.actions:
|
if armature_uuid != blender_action_name and blender_action_name in bpy.data.actions:
|
||||||
# Not bake situation
|
# Not bake situation
|
||||||
channels_animated, to_be_sampled = get_channel_groups(armature_uuid, bpy.data.actions[blender_action_name], export_settings)
|
channels_animated, to_be_sampled, extra_channels = get_channel_groups(armature_uuid, bpy.data.actions[blender_action_name], export_settings)
|
||||||
for chan in [chan for chan in channels_animated.values() if chan['bone'] is not None]:
|
for chan in [chan for chan in channels_animated.values() if chan['bone'] is not None]:
|
||||||
for prop in chan['properties'].keys():
|
for prop in chan['properties'].keys():
|
||||||
list_of_animated_bone_channels[
|
list_of_animated_bone_channels[
|
||||||
@ -88,7 +89,7 @@ def gather_armature_sampled_channels(armature_uuid, blender_action_name, export_
|
|||||||
if channel is not None:
|
if channel is not None:
|
||||||
channels.append(channel)
|
channels.append(channel)
|
||||||
|
|
||||||
return channels
|
return channels, extra_channels
|
||||||
|
|
||||||
def gather_sampled_bone_channel(
|
def gather_sampled_bone_channel(
|
||||||
armature_uuid: str,
|
armature_uuid: str,
|
||||||
@ -152,7 +153,7 @@ def __gather_sampler(armature_uuid, bone, channel, action_name, node_channel_is_
|
|||||||
def __gather_armature_object_channel(obj_uuid: str, blender_action, export_settings):
|
def __gather_armature_object_channel(obj_uuid: str, blender_action, export_settings):
|
||||||
channels = []
|
channels = []
|
||||||
|
|
||||||
channels_animated, to_be_sampled = get_channel_groups(obj_uuid, blender_action, export_settings)
|
channels_animated, to_be_sampled, extra_channels = get_channel_groups(obj_uuid, blender_action, export_settings)
|
||||||
# Remove all channel linked to bones, keep only directly object channels
|
# Remove all channel linked to bones, keep only directly object channels
|
||||||
channels_animated = [c for c in channels_animated.values() if c['type'] == "OBJECT"]
|
channels_animated = [c for c in channels_animated.values() if c['type'] == "OBJECT"]
|
||||||
to_be_sampled = [c for c in to_be_sampled if c[1] == "OBJECT"]
|
to_be_sampled = [c for c in to_be_sampled if c[1] == "OBJECT"]
|
||||||
|
@ -35,6 +35,27 @@ def get_cache_data(path: str,
|
|||||||
if export_settings['gltf_animation_mode'] in "NLA_TRACKS":
|
if export_settings['gltf_animation_mode'] in "NLA_TRACKS":
|
||||||
obj_uuids = [blender_obj_uuid]
|
obj_uuids = [blender_obj_uuid]
|
||||||
|
|
||||||
|
# If there is only 1 object to cache, we can disable viewport for other objects (for performance)
|
||||||
|
# This can be on these cases:
|
||||||
|
# - TRACK mode
|
||||||
|
# - Only one object to cache (but here, no really useful for performance)
|
||||||
|
# - Action mode, where some object have multiple actions
|
||||||
|
# - For this case, on first call, we will cache active action for all objects
|
||||||
|
# - On next calls, we will cache only the action of current object, so we can disable viewport for others
|
||||||
|
# For armature : We already checked that we can disable viewport (in case of drivers, this is currently not possible)
|
||||||
|
|
||||||
|
need_to_enable_again = False
|
||||||
|
if export_settings['gltf_optimize_armature_disable_viewport'] is True and len(obj_uuids) == 1:
|
||||||
|
need_to_enable_again = True
|
||||||
|
# Before baking, disabling from viewport all meshes
|
||||||
|
for obj in [n.blender_object for n in export_settings['vtree'].nodes.values() if n.blender_type in
|
||||||
|
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
|
||||||
|
if obj is None:
|
||||||
|
continue
|
||||||
|
obj.hide_viewport = True
|
||||||
|
export_settings['vtree'].nodes[obj_uuids[0]].blender_object.hide_viewport = False
|
||||||
|
|
||||||
|
|
||||||
depsgraph = bpy.context.evaluated_depsgraph_get()
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
frame = min_
|
frame = min_
|
||||||
@ -94,7 +115,7 @@ def get_cache_data(path: str,
|
|||||||
|
|
||||||
if export_settings['vtree'].nodes[obj_uuid].blender_type != VExportNode.COLLECTION:
|
if export_settings['vtree'].nodes[obj_uuid].blender_type != VExportNode.COLLECTION:
|
||||||
if blender_obj and blender_obj.animation_data and blender_obj.animation_data.action \
|
if blender_obj and blender_obj.animation_data and blender_obj.animation_data.action \
|
||||||
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
|
||||||
if blender_obj.animation_data.action.name not in data[obj_uuid].keys():
|
if blender_obj.animation_data.action.name not in data[obj_uuid].keys():
|
||||||
data[obj_uuid][blender_obj.animation_data.action.name] = {}
|
data[obj_uuid][blender_obj.animation_data.action.name] = {}
|
||||||
data[obj_uuid][blender_obj.animation_data.action.name]['matrix'] = {}
|
data[obj_uuid][blender_obj.animation_data.action.name]['matrix'] = {}
|
||||||
@ -125,7 +146,7 @@ def get_cache_data(path: str,
|
|||||||
if blender_obj and blender_obj.type == "ARMATURE":
|
if blender_obj and blender_obj.type == "ARMATURE":
|
||||||
bones = export_settings['vtree'].get_all_bones(obj_uuid)
|
bones = export_settings['vtree'].get_all_bones(obj_uuid)
|
||||||
if blender_obj.animation_data and blender_obj.animation_data.action \
|
if blender_obj.animation_data and blender_obj.animation_data.action \
|
||||||
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
|
||||||
if 'bone' not in data[obj_uuid][blender_obj.animation_data.action.name].keys():
|
if 'bone' not in data[obj_uuid][blender_obj.animation_data.action.name].keys():
|
||||||
data[obj_uuid][blender_obj.animation_data.action.name]['bone'] = {}
|
data[obj_uuid][blender_obj.animation_data.action.name]['bone'] = {}
|
||||||
elif blender_obj.animation_data \
|
elif blender_obj.animation_data \
|
||||||
@ -155,7 +176,7 @@ def get_cache_data(path: str,
|
|||||||
matrix = matrix @ blender_obj.matrix_world
|
matrix = matrix @ blender_obj.matrix_world
|
||||||
|
|
||||||
if blender_obj.animation_data and blender_obj.animation_data.action \
|
if blender_obj.animation_data and blender_obj.animation_data.action \
|
||||||
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
|
||||||
if blender_bone.name not in data[obj_uuid][blender_obj.animation_data.action.name]['bone'].keys():
|
if blender_bone.name not in data[obj_uuid][blender_obj.animation_data.action.name]['bone'].keys():
|
||||||
data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name] = {}
|
data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name] = {}
|
||||||
data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name][frame] = matrix
|
data[obj_uuid][blender_obj.animation_data.action.name]['bone'][blender_bone.name][frame] = matrix
|
||||||
@ -187,7 +208,7 @@ def get_cache_data(path: str,
|
|||||||
and blender_obj.data.shape_keys is not None \
|
and blender_obj.data.shape_keys is not None \
|
||||||
and blender_obj.data.shape_keys.animation_data is not None \
|
and blender_obj.data.shape_keys.animation_data is not None \
|
||||||
and blender_obj.data.shape_keys.animation_data.action is not None \
|
and blender_obj.data.shape_keys.animation_data.action is not None \
|
||||||
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
|
||||||
|
|
||||||
if blender_obj.data.shape_keys.animation_data.action.name not in data[obj_uuid].keys():
|
if blender_obj.data.shape_keys.animation_data.action.name not in data[obj_uuid].keys():
|
||||||
data[obj_uuid][blender_obj.data.shape_keys.animation_data.action.name] = {}
|
data[obj_uuid][blender_obj.data.shape_keys.animation_data.action.name] = {}
|
||||||
@ -233,7 +254,7 @@ def get_cache_data(path: str,
|
|||||||
if dr_obj not in data.keys():
|
if dr_obj not in data.keys():
|
||||||
data[dr_obj] = {}
|
data[dr_obj] = {}
|
||||||
if blender_obj.animation_data and blender_obj.animation_data.action \
|
if blender_obj.animation_data and blender_obj.animation_data.action \
|
||||||
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]:
|
and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS", "BROADCAST"]:
|
||||||
if obj_uuid + "_" + blender_obj.animation_data.action.name not in data[dr_obj]: # Using uuid of armature + armature animation name as animation name
|
if obj_uuid + "_" + blender_obj.animation_data.action.name not in data[dr_obj]: # Using uuid of armature + armature animation name as animation name
|
||||||
data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name] = {}
|
data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name] = {}
|
||||||
data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name]['sk'] = {}
|
data[dr_obj][obj_uuid + "_" + blender_obj.animation_data.action.name]['sk'] = {}
|
||||||
@ -254,6 +275,14 @@ def get_cache_data(path: str,
|
|||||||
data[dr_obj][obj_uuid + "_" + obj_uuid]['sk'][None][frame] = [k.value for k in get_sk_exported(driver_object.data.shape_keys.key_blocks)]
|
data[dr_obj][obj_uuid + "_" + obj_uuid]['sk'][None][frame] = [k.value for k in get_sk_exported(driver_object.data.shape_keys.key_blocks)]
|
||||||
|
|
||||||
frame += step
|
frame += step
|
||||||
|
|
||||||
|
# And now, restoring meshes in viewport
|
||||||
|
for node, obj in [(n, n.blender_object) for n in export_settings['vtree'].nodes.values() if n.blender_type in
|
||||||
|
[VExportNode.OBJECT, VExportNode.ARMATURE, VExportNode.COLLECTION]]:
|
||||||
|
obj.hide_viewport = node.default_hide_viewport
|
||||||
|
export_settings['vtree'].nodes[obj_uuids[0]].blender_object.hide_viewport = export_settings['vtree'].nodes[obj_uuids[0]].default_hide_viewport
|
||||||
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# For perf, we may be more precise, and get a list of ranges to be exported that include all needed frames
|
# For perf, we may be more precise, and get a list of ranges to be exported that include all needed frames
|
||||||
|
@ -7,29 +7,45 @@ import typing
|
|||||||
from ......io.com import gltf2_io
|
from ......io.com import gltf2_io
|
||||||
from ......io.exp.gltf2_io_user_extensions import export_user_extensions
|
from ......io.exp.gltf2_io_user_extensions import export_user_extensions
|
||||||
from .....com.gltf2_blender_extras import generate_extras
|
from .....com.gltf2_blender_extras import generate_extras
|
||||||
|
from ...fcurves.gltf2_blender_gather_fcurves_sampler import gather_animation_fcurves_sampler
|
||||||
from .gltf2_blender_gather_object_channels import gather_object_sampled_channels
|
from .gltf2_blender_gather_object_channels import gather_object_sampled_channels
|
||||||
|
|
||||||
def gather_action_object_sampled(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings):
|
def gather_action_object_sampled(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings):
|
||||||
|
|
||||||
|
extra_samplers = []
|
||||||
|
|
||||||
# If no animation in file, no need to bake
|
# If no animation in file, no need to bake
|
||||||
if len(bpy.data.actions) == 0:
|
if len(bpy.data.actions) == 0:
|
||||||
return None
|
return None, extra_samplers
|
||||||
|
|
||||||
|
channels, extra_channels = __gather_channels(object_uuid, blender_action.name if blender_action else cache_key, export_settings)
|
||||||
animation = gltf2_io.Animation(
|
animation = gltf2_io.Animation(
|
||||||
channels=__gather_channels(object_uuid, blender_action.name if blender_action else cache_key, export_settings),
|
channels=channels,
|
||||||
extensions=None,
|
extensions=None,
|
||||||
extras=__gather_extras(blender_action, export_settings),
|
extras=__gather_extras(blender_action, export_settings),
|
||||||
name=__gather_name(object_uuid, blender_action, cache_key, export_settings),
|
name=__gather_name(object_uuid, blender_action, cache_key, export_settings),
|
||||||
samplers=[]
|
samplers=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if export_settings['gltf_export_extra_animations']:
|
||||||
|
for chan in [chan for chan in extra_channels.values() if len(chan['properties']) != 0]:
|
||||||
|
for channel_group_name, channel_group in chan['properties'].items():
|
||||||
|
|
||||||
|
# No glTF channel here, as we don't have any target
|
||||||
|
# Trying to retrieve sampler directly
|
||||||
|
sampler = gather_animation_fcurves_sampler(object_uuid, tuple(channel_group), None, None, True, export_settings)
|
||||||
|
if sampler is not None:
|
||||||
|
extra_samplers.append((channel_group_name, sampler, "OBJECT", None))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if not animation.channels:
|
if not animation.channels:
|
||||||
return None
|
return None, extra_samplers
|
||||||
|
|
||||||
blender_object = export_settings['vtree'].nodes[object_uuid].blender_object
|
blender_object = export_settings['vtree'].nodes[object_uuid].blender_object
|
||||||
export_user_extensions('animation_action_object_sampled', export_settings, animation, blender_object, blender_action, cache_key)
|
export_user_extensions('animation_action_object_sampled', export_settings, animation, blender_object, blender_action, cache_key)
|
||||||
|
|
||||||
return animation
|
return animation, extra_samplers
|
||||||
|
|
||||||
def __gather_name(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings):
|
def __gather_name(object_uuid: str, blender_action: typing.Optional[bpy.types.Action], cache_key: str, export_settings):
|
||||||
if blender_action:
|
if blender_action:
|
||||||
|
@ -15,11 +15,15 @@ from .gltf2_blender_gather_object_channel_target import gather_object_sampled_ch
|
|||||||
|
|
||||||
def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, export_settings) -> typing.List[gltf2_io.AnimationChannel]:
|
def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, export_settings) -> typing.List[gltf2_io.AnimationChannel]:
|
||||||
channels = []
|
channels = []
|
||||||
|
extra_channels = {}
|
||||||
|
|
||||||
|
# Bake situation does not export any extra animation channels, as we bake TRS + weights on Track or scene level, without direct
|
||||||
|
# Access to fcurve and action data
|
||||||
|
|
||||||
list_of_animated_channels = {}
|
list_of_animated_channels = {}
|
||||||
if object_uuid != blender_action_name and blender_action_name in bpy.data.actions:
|
if object_uuid != blender_action_name and blender_action_name in bpy.data.actions:
|
||||||
# Not bake situation
|
# Not bake situation
|
||||||
channels_animated, to_be_sampled = get_channel_groups(object_uuid, bpy.data.actions[blender_action_name], export_settings)
|
channels_animated, to_be_sampled, extra_channels = get_channel_groups(object_uuid, bpy.data.actions[blender_action_name], export_settings)
|
||||||
for chan in [chan for chan in channels_animated.values() if chan['bone'] is None]:
|
for chan in [chan for chan in channels_animated.values() if chan['bone'] is None]:
|
||||||
for prop in chan['properties'].keys():
|
for prop in chan['properties'].keys():
|
||||||
list_of_animated_channels[
|
list_of_animated_channels[
|
||||||
@ -45,7 +49,7 @@ def gather_object_sampled_channels(object_uuid: str, blender_action_name: str, e
|
|||||||
export_user_extensions('animation_gather_object_channel', export_settings, blender_object, blender_action_name)
|
export_user_extensions('animation_gather_object_channel', export_settings, blender_object, blender_action_name)
|
||||||
|
|
||||||
|
|
||||||
return channels if len(channels) > 0 else None
|
return channels if len(channels) > 0 else None, extra_channels
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def gather_sampled_object_channel(
|
def gather_sampled_object_channel(
|
||||||
|
@ -2,10 +2,14 @@
|
|||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import typing
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from ......blender.com.gltf2_blender_data_path import get_sk_exported
|
from ......blender.com.gltf2_blender_data_path import get_sk_exported
|
||||||
from ....gltf2_blender_gather_cache import cached
|
from ....gltf2_blender_gather_cache import cached
|
||||||
from ...gltf2_blender_gather_keyframes import Keyframe
|
from ...gltf2_blender_gather_keyframes import Keyframe
|
||||||
|
from ...fcurves.gltf2_blender_gather_fcurves_channels import get_channel_groups
|
||||||
|
from ...fcurves.gltf2_blender_gather_fcurves_keyframes import gather_non_keyed_values
|
||||||
from ..gltf2_blender_gather_animation_sampling_cache import get_cache_data
|
from ..gltf2_blender_gather_animation_sampling_cache import get_cache_data
|
||||||
|
|
||||||
|
|
||||||
@ -22,6 +26,40 @@ def gather_sk_sampled_keyframes(obj_uuid,
|
|||||||
frame = start_frame
|
frame = start_frame
|
||||||
step = export_settings['gltf_frame_step']
|
step = export_settings['gltf_frame_step']
|
||||||
blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object
|
blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object
|
||||||
|
|
||||||
|
|
||||||
|
if export_settings['gltf_optimize_armature_disable_viewport'] is True:
|
||||||
|
# Using this option, we miss the drivers :(
|
||||||
|
# No solution exists for now. In the future, we should be able to copy a driver
|
||||||
|
if action_name in bpy.data.actions:
|
||||||
|
channel_group, _ = get_channel_groups(obj_uuid, bpy.data.actions[action_name], export_settings, no_sample_option=True)
|
||||||
|
elif blender_obj.data.shape_keys.animation_data and blender_obj.data.shape_keys.animation_data.action:
|
||||||
|
channel_group, _ = get_channel_groups(obj_uuid, blender_obj.data.shape_keys.animation_data.action, export_settings, no_sample_option=True)
|
||||||
|
else:
|
||||||
|
channel_group = {}
|
||||||
|
channels = [None] * len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))
|
||||||
|
|
||||||
|
# One day, if we will be able to bake drivers or evaluate it the right way, we can add here the driver fcurves
|
||||||
|
|
||||||
|
for chan in channel_group.values():
|
||||||
|
channels = chan['properties']['value']
|
||||||
|
break
|
||||||
|
|
||||||
|
non_keyed_values = gather_non_keyed_values(obj_uuid, channels, None, export_settings)
|
||||||
|
|
||||||
|
while frame <= end_frame:
|
||||||
|
key = Keyframe(channels, frame, None)
|
||||||
|
key.value = [c.evaluate(frame) for c in channels if c is not None]
|
||||||
|
# Complete key with non keyed values, if needed
|
||||||
|
if len([c for c in channels if c is not None]) != key.get_target_len():
|
||||||
|
complete_key(key, non_keyed_values)
|
||||||
|
|
||||||
|
keyframes.append(key)
|
||||||
|
frame += step
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Full bake, we will go frame by frame. This can take time (more than using evaluate)
|
||||||
|
|
||||||
while frame <= end_frame:
|
while frame <= end_frame:
|
||||||
key = Keyframe([None] * (len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))), frame, 'value')
|
key = Keyframe([None] * (len(get_sk_exported(blender_obj.data.shape_keys.key_blocks))), frame, 'value')
|
||||||
key.value_total = get_cache_data(
|
key.value_total = get_cache_data(
|
||||||
@ -54,3 +92,13 @@ def gather_sk_sampled_keyframes(obj_uuid,
|
|||||||
|
|
||||||
def fcurve_is_constant(keyframes):
|
def fcurve_is_constant(keyframes):
|
||||||
return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)])
|
return all([j < 0.0001 for j in np.ptp([[k.value[i] for i in range(len(keyframes[0].value))] for k in keyframes], axis=0)])
|
||||||
|
|
||||||
|
#TODO de-duplicate, but import issue???
|
||||||
|
def complete_key(key: Keyframe, non_keyed_values: typing.Tuple[typing.Optional[float]]):
|
||||||
|
"""
|
||||||
|
Complete keyframe with non keyed values
|
||||||
|
"""
|
||||||
|
for i in range(0, key.get_target_len()):
|
||||||
|
if i in key.get_indices():
|
||||||
|
continue # this is a keyed array_index or a SK animated
|
||||||
|
key.set_value_index(i, non_keyed_values[i])
|
||||||
|
@ -106,6 +106,9 @@ def __create_buffer(exporter, export_settings):
|
|||||||
buffer = bytes()
|
buffer = bytes()
|
||||||
if export_settings['gltf_format'] == 'GLB':
|
if export_settings['gltf_format'] == 'GLB':
|
||||||
buffer = exporter.finalize_buffer(export_settings['gltf_filedirectory'], is_glb=True)
|
buffer = exporter.finalize_buffer(export_settings['gltf_filedirectory'], is_glb=True)
|
||||||
|
else:
|
||||||
|
if export_settings['gltf_format'] == 'GLTF_EMBEDDED':
|
||||||
|
exporter.finalize_buffer(export_settings['gltf_filedirectory'])
|
||||||
else:
|
else:
|
||||||
exporter.finalize_buffer(export_settings['gltf_filedirectory'],
|
exporter.finalize_buffer(export_settings['gltf_filedirectory'],
|
||||||
export_settings['gltf_binaryfilename'])
|
export_settings['gltf_binaryfilename'])
|
||||||
|
@ -100,7 +100,7 @@ def datacache(func):
|
|||||||
# Here are the key used: result[obj_uuid][action_name][path][bone][frame]
|
# Here are the key used: result[obj_uuid][action_name][path][bone][frame]
|
||||||
return result[cache_key_args[1]][cache_key_args[3]][cache_key_args[0]][cache_key_args[2]][cache_key_args[4]]
|
return result[cache_key_args[1]][cache_key_args[3]][cache_key_args[0]][cache_key_args[2]][cache_key_args[4]]
|
||||||
# object is in cache, but not this action
|
# object is in cache, but not this action
|
||||||
# We need to keep other actions
|
# We need to not erase other actions of this object
|
||||||
elif cache_key_args[3] not in func.__cache[cache_key_args[1]].keys():
|
elif cache_key_args[3] not in func.__cache[cache_key_args[1]].keys():
|
||||||
result = func(*args, only_gather_provided=True)
|
result = func(*args, only_gather_provided=True)
|
||||||
# The result can contains multiples animations, in case this is an armature with drivers
|
# The result can contains multiples animations, in case this is an armature with drivers
|
||||||
|
@ -55,6 +55,8 @@ class VExportNode:
|
|||||||
self.blender_object = None
|
self.blender_object = None
|
||||||
self.blender_bone = None
|
self.blender_bone = None
|
||||||
|
|
||||||
|
self.default_hide_viewport = False # Need to store the default value for meshes in case of animation baking on armature
|
||||||
|
|
||||||
self.force_as_empty = False # Used for instancer display
|
self.force_as_empty = False # Used for instancer display
|
||||||
|
|
||||||
# Only for bone/bone and object parented to bone
|
# Only for bone/bone and object parented to bone
|
||||||
@ -160,14 +162,17 @@ class VExportTree:
|
|||||||
node.blender_type = VExportNode.COLLECTION
|
node.blender_type = VExportNode.COLLECTION
|
||||||
elif blender_object.type == "ARMATURE":
|
elif blender_object.type == "ARMATURE":
|
||||||
node.blender_type = VExportNode.ARMATURE
|
node.blender_type = VExportNode.ARMATURE
|
||||||
|
node.default_hide_viewport = blender_object.hide_viewport
|
||||||
elif blender_object.type == "CAMERA":
|
elif blender_object.type == "CAMERA":
|
||||||
node.blender_type = VExportNode.CAMERA
|
node.blender_type = VExportNode.CAMERA
|
||||||
elif blender_object.type == "LIGHT":
|
elif blender_object.type == "LIGHT":
|
||||||
node.blender_type = VExportNode.LIGHT
|
node.blender_type = VExportNode.LIGHT
|
||||||
elif blender_object.instance_type == "COLLECTION":
|
elif blender_object.instance_type == "COLLECTION":
|
||||||
node.blender_type = VExportNode.INST_COLLECTION
|
node.blender_type = VExportNode.INST_COLLECTION
|
||||||
|
node.default_hide_viewport = blender_object.hide_viewport
|
||||||
else:
|
else:
|
||||||
node.blender_type = VExportNode.OBJECT
|
node.blender_type = VExportNode.OBJECT
|
||||||
|
node.default_hide_viewport = blender_object.hide_viewport
|
||||||
|
|
||||||
# For meshes with armature modifier (parent is armature), keep armature uuid
|
# For meshes with armature modifier (parent is armature), keep armature uuid
|
||||||
if node.blender_type == VExportNode.OBJECT:
|
if node.blender_type == VExportNode.OBJECT:
|
||||||
|
@ -125,7 +125,7 @@ class GlTF2Exporter:
|
|||||||
f.write(self.__buffer.to_bytes())
|
f.write(self.__buffer.to_bytes())
|
||||||
uri = buffer_name
|
uri = buffer_name
|
||||||
else:
|
else:
|
||||||
pass # This is no more possible, we don't export embedded buffers
|
uri = self.__buffer.to_embed_string()
|
||||||
|
|
||||||
buffer = gltf2_io.Buffer(
|
buffer = gltf2_io.Buffer(
|
||||||
byte_length=self.__buffer.byte_length,
|
byte_length=self.__buffer.byte_length,
|
||||||
|
@ -163,12 +163,13 @@ def __gather_normal_scale(primary_socket, export_settings):
|
|||||||
def __gather_occlusion_strength(primary_socket, export_settings):
|
def __gather_occlusion_strength(primary_socket, export_settings):
|
||||||
# Look for a MixRGB node that mixes with pure white in front of
|
# Look for a MixRGB node that mixes with pure white in front of
|
||||||
# primary_socket. The mix factor gives the occlusion strength.
|
# primary_socket. The mix factor gives the occlusion strength.
|
||||||
node = previous_node(primary_socket)
|
nav = primary_socket.to_node_nav()
|
||||||
if node and node.node.type == 'MIX' and node.node.blend_type == 'MIX':
|
nav.move_back()
|
||||||
fac = get_const_from_socket(NodeSocket(node.node.inputs['Factor'], node.group_path), kind='VALUE')
|
if nav.moved and nav.node.type == 'MIX' and nav.node.blend_type == 'MIX':
|
||||||
col1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind='RGB')
|
fac = nav.get_constant('Factor')
|
||||||
col2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind='RGB')
|
|
||||||
if fac is not None:
|
if fac is not None:
|
||||||
|
col1 = nav.get_constant('#A_Color')
|
||||||
|
col2 = nav.get_constant('#B_Color')
|
||||||
if col1 == [1.0, 1.0, 1.0] and col2 is None:
|
if col1 == [1.0, 1.0, 1.0] and col2 is None:
|
||||||
return fac
|
return fac
|
||||||
if col1 is None and col2 == [1.0, 1.0, 1.0]:
|
if col1 is None and col2 == [1.0, 1.0, 1.0]:
|
||||||
|
@ -184,11 +184,210 @@ def get_socket_from_gltf_material_node(blender_material: bpy.types.Material, nam
|
|||||||
|
|
||||||
return NodeSocket(None, None)
|
return NodeSocket(None, None)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeNav:
|
||||||
|
"""Helper for navigating through node trees."""
|
||||||
|
def __init__(self, node, in_socket=None, out_socket=None):
|
||||||
|
self.node = node # Current node
|
||||||
|
self.out_socket = out_socket # Socket through which we arrived at this node
|
||||||
|
self.in_socket = in_socket # Socket through which we will leave this node
|
||||||
|
self.stack = [] # Stack of (group node, socket) pairs descended through to get here
|
||||||
|
self.moved = False # Whether the last move_back call moved back or not
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
new = NodeNav(self.node)
|
||||||
|
new.assign(self)
|
||||||
|
return new
|
||||||
|
|
||||||
|
def assign(self, other):
|
||||||
|
self.node = other.node
|
||||||
|
self.in_socket = other.in_socket
|
||||||
|
self.out_socket = other.out_socket
|
||||||
|
self.stack = other.stack.copy()
|
||||||
|
self.moved = other.moved
|
||||||
|
|
||||||
|
def select_input_socket(self, in_soc):
|
||||||
|
"""Selects an input socket.
|
||||||
|
|
||||||
|
Most operations that operate on the input socket can be passed an in_soc
|
||||||
|
parameter to select an input socket before running.
|
||||||
|
"""
|
||||||
|
if in_soc is None:
|
||||||
|
# Keep current selected input socket
|
||||||
|
return
|
||||||
|
elif isinstance(in_soc, bpy.types.NodeSocket):
|
||||||
|
assert in_soc.node == self.node
|
||||||
|
self.in_socket = in_soc
|
||||||
|
elif isinstance(in_soc, int):
|
||||||
|
self.in_socket = self.node.inputs[in_soc]
|
||||||
|
else:
|
||||||
|
assert isinstance(in_soc, str)
|
||||||
|
# An identifier like "#A_Color" selects a socket by
|
||||||
|
# identifier. This is useful for sockets that cannot be
|
||||||
|
# selected because of non-unique names.
|
||||||
|
if in_soc.startswith('#'):
|
||||||
|
ident = in_soc.removeprefix('#')
|
||||||
|
for socket in self.node.inputs:
|
||||||
|
if socket.identifier == ident:
|
||||||
|
self.in_socket = socket
|
||||||
|
return
|
||||||
|
# Select by regular name
|
||||||
|
self.in_socket = self.node.inputs[in_soc]
|
||||||
|
|
||||||
|
def get_out_socket_index(self):
|
||||||
|
assert self.out_socket
|
||||||
|
for i, soc in enumerate(self.node.outputs):
|
||||||
|
if soc == self.out_socket:
|
||||||
|
return i
|
||||||
|
assert False
|
||||||
|
|
||||||
|
def descend(self):
|
||||||
|
"""Descend into a group node."""
|
||||||
|
if self.node and self.node.type == 'GROUP' and self.node.node_tree and self.out_socket:
|
||||||
|
i = self.get_out_socket_index()
|
||||||
|
self.stack.append((self.node, self.out_socket))
|
||||||
|
self.node = next(node for node in self.node.node_tree.nodes if node.type == 'GROUP_OUTPUT')
|
||||||
|
self.in_socket = self.node.inputs[i]
|
||||||
|
self.out_socket = None
|
||||||
|
|
||||||
|
def ascend(self):
|
||||||
|
"""Ascend from a group input node back to the group node."""
|
||||||
|
if self.stack and self.node and self.node.type == 'GROUP_INPUT' and self.out_socket:
|
||||||
|
i = self.get_out_socket_index()
|
||||||
|
self.node, self.out_socket = self.stack.pop()
|
||||||
|
self.in_socket = self.node.inputs[i]
|
||||||
|
|
||||||
|
def move_back(self, in_soc=None):
|
||||||
|
"""Move backwards through an input socket to the next node."""
|
||||||
|
self.moved = False
|
||||||
|
|
||||||
|
self.select_input_socket(in_soc)
|
||||||
|
|
||||||
|
if not self.in_socket or not self.in_socket.is_linked:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Warning, slow! socket.links is O(total number of links)!
|
||||||
|
link = self.in_socket.links[0]
|
||||||
|
|
||||||
|
self.node = link.from_node
|
||||||
|
self.out_socket = link.from_socket
|
||||||
|
self.in_socket = None
|
||||||
|
self.moved = True
|
||||||
|
|
||||||
|
# Continue moving
|
||||||
|
if self.node.type == 'REROUTE':
|
||||||
|
self.move_back(0)
|
||||||
|
elif self.node.type == 'GROUP':
|
||||||
|
self.descend()
|
||||||
|
self.move_back()
|
||||||
|
elif self.node.type == 'GROUP_INPUT':
|
||||||
|
self.ascend()
|
||||||
|
self.move_back()
|
||||||
|
|
||||||
|
def peek_back(self, in_soc=None):
|
||||||
|
"""Peeks backwards through an input socket without modifying self."""
|
||||||
|
s = self.copy()
|
||||||
|
s.select_input_socket(in_soc)
|
||||||
|
s.move_back()
|
||||||
|
return s
|
||||||
|
|
||||||
|
def get_constant(self, in_soc=None):
|
||||||
|
"""Gets a constant from an input socket. Returns None if non-constant."""
|
||||||
|
self.select_input_socket(in_soc)
|
||||||
|
|
||||||
|
if not self.in_socket:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get constant from unlinked socket's default value
|
||||||
|
if not self.in_socket.is_linked:
|
||||||
|
if self.in_socket.type == 'RGBA':
|
||||||
|
color = list(self.in_socket.default_value)
|
||||||
|
color = color[:3] # drop unused alpha component (assumes shader tree)
|
||||||
|
return color
|
||||||
|
|
||||||
|
elif self.in_socket.type == 'SHADER':
|
||||||
|
# Treat unlinked shader sockets as black
|
||||||
|
return [0.0, 0.0, 0.0]
|
||||||
|
|
||||||
|
elif self.in_socket.type == 'VECTOR':
|
||||||
|
return list(self.in_socket.default_value)
|
||||||
|
|
||||||
|
elif self.in_socket.type == 'VALUE':
|
||||||
|
return self.in_socket.default_value
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check for a constant in the next node
|
||||||
|
nav = self.peek_back()
|
||||||
|
if nav.moved:
|
||||||
|
if self.in_socket.type == 'RGBA':
|
||||||
|
if nav.node.type == 'RGB':
|
||||||
|
color = list(nav.out_socket.default_value)
|
||||||
|
color = color[:3] # drop unused alpha component (assumes shader tree)
|
||||||
|
return color
|
||||||
|
|
||||||
|
elif self.in_socket.type == 'VALUE':
|
||||||
|
if nav.node.type == 'VALUE':
|
||||||
|
return nav.node.out_socket.default_value
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_factor(self, in_soc=None):
|
||||||
|
"""Gets a factor, eg. metallicFactor. Either a constant or constant multiplier."""
|
||||||
|
self.select_input_socket(in_soc)
|
||||||
|
|
||||||
|
if not self.in_socket:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Constant
|
||||||
|
fac = self.get_constant()
|
||||||
|
if fac is not None:
|
||||||
|
return fac
|
||||||
|
|
||||||
|
# Multiplied by constant
|
||||||
|
nav = self.peek_back()
|
||||||
|
if nav.moved:
|
||||||
|
x1, x2 = None, None
|
||||||
|
|
||||||
|
if self.in_socket.type == 'RGBA':
|
||||||
|
is_mul = (
|
||||||
|
nav.node.type == 'MIX' and
|
||||||
|
nav.node.data_type == 'RGBA' and
|
||||||
|
nav.node.blend_type == 'MULTIPLY'
|
||||||
|
)
|
||||||
|
if is_mul:
|
||||||
|
# TODO: check factor is 1?
|
||||||
|
x1 = nav.get_constant('#A_Color')
|
||||||
|
x2 = nav.get_constant('#B_Color')
|
||||||
|
|
||||||
|
elif self.in_socket.type == 'VALUE':
|
||||||
|
if nav.node.type == 'MATH' and nav.node.operation == 'MULTIPLY':
|
||||||
|
x1 = nav.get_constant(0)
|
||||||
|
x2 = nav.get_constant(1)
|
||||||
|
|
||||||
|
if x1 is not None and x2 is None: return x1
|
||||||
|
if x2 is not None and x1 is None: return x2
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class NodeSocket:
|
class NodeSocket:
|
||||||
def __init__(self, socket, group_path):
|
def __init__(self, socket, group_path):
|
||||||
self.socket = socket
|
self.socket = socket
|
||||||
self.group_path = group_path
|
self.group_path = group_path
|
||||||
|
|
||||||
|
def to_node_nav(self):
|
||||||
|
assert self.socket
|
||||||
|
nav = NodeNav(
|
||||||
|
self.socket.node,
|
||||||
|
out_socket=self.socket if self.socket.is_output else None,
|
||||||
|
in_socket=self.socket if not self.socket.is_output else None,
|
||||||
|
)
|
||||||
|
# No output socket information
|
||||||
|
nav.stack = [(node, None) for node in self.group_path]
|
||||||
|
return nav
|
||||||
|
|
||||||
class ShNode:
|
class ShNode:
|
||||||
def __init__(self, node, group_path):
|
def __init__(self, node, group_path):
|
||||||
self.node = node
|
self.node = node
|
||||||
@ -243,52 +442,15 @@ def get_socket(blender_material: bpy.types.Material, name: str, volume=False):
|
|||||||
|
|
||||||
return NodeSocket(None, None)
|
return NodeSocket(None, None)
|
||||||
|
|
||||||
|
|
||||||
|
# Old, prefer NodeNav.get_factor in new code
|
||||||
def get_factor_from_socket(socket, kind):
|
def get_factor_from_socket(socket, kind):
|
||||||
"""
|
return socket.to_node_nav().get_factor()
|
||||||
For baseColorFactor, metallicFactor, etc.
|
|
||||||
Get a constant value from a socket, or a constant value
|
|
||||||
from a MULTIPLY node just before the socket.
|
|
||||||
kind is either 'RGB' or 'VALUE'.
|
|
||||||
"""
|
|
||||||
fac = get_const_from_socket(socket, kind)
|
|
||||||
if fac is not None:
|
|
||||||
return fac
|
|
||||||
|
|
||||||
node = previous_node(socket)
|
|
||||||
if node.node is not None:
|
|
||||||
x1, x2 = None, None
|
|
||||||
if kind == 'RGB':
|
|
||||||
if node.node.type == 'MIX' and node.node.data_type == "RGBA" and node.node.blend_type == 'MULTIPLY':
|
|
||||||
# TODO: handle factor in inputs[0]?
|
|
||||||
x1 = get_const_from_socket(NodeSocket(node.node.inputs[6], node.group_path), kind)
|
|
||||||
x2 = get_const_from_socket(NodeSocket(node.node.inputs[7], node.group_path), kind)
|
|
||||||
if kind == 'VALUE':
|
|
||||||
if node.node.type == 'MATH' and node.node.operation == 'MULTIPLY':
|
|
||||||
x1 = get_const_from_socket(NodeSocket(node.node.inputs[0], node.group_path), kind)
|
|
||||||
x2 = get_const_from_socket(NodeSocket(node.node.inputs[1], node.group_path), kind)
|
|
||||||
if x1 is not None and x2 is None: return x1
|
|
||||||
if x2 is not None and x1 is None: return x2
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
# Old, prefer NodeNav.get_constant in new code
|
||||||
def get_const_from_socket(socket, kind):
|
def get_const_from_socket(socket, kind):
|
||||||
if not socket.socket.is_linked:
|
return socket.to_node_nav().get_constant()
|
||||||
if kind == 'RGB':
|
|
||||||
if socket.socket.type != 'RGBA': return None
|
|
||||||
return list(socket.socket.default_value)[:3]
|
|
||||||
if kind == 'VALUE':
|
|
||||||
if socket.socket.type != 'VALUE': return None
|
|
||||||
return socket.socket.default_value
|
|
||||||
|
|
||||||
# Handle connection to a constant RGB/Value node
|
|
||||||
prev_node = previous_node(socket)
|
|
||||||
if prev_node.node is not None:
|
|
||||||
if kind == 'RGB' and prev_node.node.type == 'RGB':
|
|
||||||
return list(prev_node.node.outputs[0].default_value)[:3]
|
|
||||||
if kind == 'VALUE' and prev_node.node.type == 'VALUE':
|
|
||||||
return prev_node.node.outputs[0].default_value
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def previous_socket(socket: NodeSocket):
|
def previous_socket(socket: NodeSocket):
|
||||||
|
@ -49,3 +49,6 @@ class Buffer:
|
|||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.__data = b""
|
self.__data = b""
|
||||||
|
|
||||||
|
def to_embed_string(self):
|
||||||
|
return 'data:application/octet-stream;base64,' + base64.b64encode(self.__data).decode('ascii')
|
||||||
|
@ -1,21 +1,6 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# --------------------------------- TISSUE ----------------------------------- #
|
# --------------------------------- TISSUE ----------------------------------- #
|
||||||
# ------------------------------- version 0.3 -------------------------------- #
|
# ------------------------------- version 0.3 -------------------------------- #
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022-2023 Blender Foundation
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
||||||
# #
|
# #
|
||||||
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# (c) Alessandro Zomparelli #
|
# (c) Alessandro Zomparelli #
|
||||||
# (2017) #
|
# (2017) #
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# --------------------------------- DUAL MESH -------------------------------- #
|
# --------------------------------- DUAL MESH -------------------------------- #
|
||||||
# -------------------------------- version 0.3 ------------------------------- #
|
# -------------------------------- version 0.3 ------------------------------- #
|
||||||
# #
|
# #
|
||||||
|
@ -1,20 +1,7 @@
|
|||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
# --------------------------- LATTICE ALONG SURFACE -------------------------- #
|
# --------------------------- LATTICE ALONG SURFACE -------------------------- #
|
||||||
# -------------------------------- version 0.3 ------------------------------- #
|
# -------------------------------- version 0.3 ------------------------------- #
|
||||||
# #
|
# #
|
||||||
|
@ -1,20 +1,6 @@
|
|||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
# SPDX-FileCopyrightText: 2020 Alessandro Zomparelli
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# #
|
# #
|
||||||
# (c) Alessandro Zomparelli #
|
# (c) Alessandro Zomparelli #
|
||||||
|
@ -1,20 +1,6 @@
|
|||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import time
|
import time
|
||||||
|
@ -1,20 +1,6 @@
|
|||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
|
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
|
||||||
# ------------------------------- version 0.84 ------------------------------- #
|
# ------------------------------- version 0.84 ------------------------------- #
|
||||||
|
@ -1,20 +1,6 @@
|
|||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
#
|
#
|
||||||
# This program is free software; you can redistribute it and/or
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
|
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
|
||||||
# ------------------------------- version 0.84 ------------------------------- #
|
# ------------------------------- version 0.84 ------------------------------- #
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
||||||
# #
|
# #
|
||||||
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
|
# ---------------------------- ADAPTIVE DUPLIFACES --------------------------- #
|
||||||
# ------------------------------- version 0.84 ------------------------------- #
|
# ------------------------------- version 0.84 ------------------------------- #
|
||||||
# #
|
# #
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2019-2023 Blender Foundation
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
import bpy, bmesh
|
import bpy, bmesh
|
||||||
|
@ -1,25 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022 Blender Foundation
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# <pep8 compliant>
|
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# Author: Stephen Leger (s-leger)
|
# Author: Stephen Leger (s-leger)
|
||||||
#
|
#
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
# --------------------------------- UV to MESH ------------------------------- #
|
# --------------------------------- UV to MESH ------------------------------- #
|
||||||
# -------------------------------- version 0.1.1 ----------------------------- #
|
# -------------------------------- version 0.1.1 ----------------------------- #
|
||||||
# #
|
# #
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2017 Alessandro Zomparelli
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
||||||
# #
|
# #
|
||||||
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
||||||
|
@ -1,23 +1,7 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022-2023 Blender Foundation
|
||||||
|
#
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
# ##### BEGIN GPL LICENSE BLOCK #####
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU General Public License
|
|
||||||
# as published by the Free Software Foundation; either version 2
|
|
||||||
# of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software Foundation,
|
|
||||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
||||||
#
|
|
||||||
# ##### END GPL LICENSE BLOCK #####
|
|
||||||
|
|
||||||
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
#-------------------------- COLORS / GROUPS EXCHANGER -------------------------#
|
||||||
# #
|
# #
|
||||||
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
# Vertex Color to Vertex Group allow you to convert colors channles to weight #
|
||||||
|
@ -72,6 +72,11 @@ def write_mesh(context, report_cb):
|
|||||||
obj = layer.objects.active
|
obj = layer.objects.active
|
||||||
export_data_layers = print_3d.use_data_layers
|
export_data_layers = print_3d.use_data_layers
|
||||||
|
|
||||||
|
# Make sure at least one object is selected.
|
||||||
|
if not context.selected_objects:
|
||||||
|
report_cb({'ERROR'}, "No objects selected")
|
||||||
|
return False
|
||||||
|
|
||||||
# Create name 'export_path/blendname-objname'
|
# Create name 'export_path/blendname-objname'
|
||||||
# add the filename component
|
# add the filename component
|
||||||
if bpy.data.is_saved:
|
if bpy.data.is_saved:
|
||||||
|
Loading…
Reference in New Issue
Block a user