diff options
Diffstat (limited to 'blenderkit/autothumb.py')
-rw-r--r-- | blenderkit/autothumb.py | 567 |
1 files changed, 455 insertions, 112 deletions
diff --git a/blenderkit/autothumb.py b/blenderkit/autothumb.py index 9f47d3c3..26697d74 100644 --- a/blenderkit/autothumb.py +++ b/blenderkit/autothumb.py @@ -16,23 +16,69 @@ # # ##### END GPL LICENSE BLOCK ##### - -from blenderkit import paths, utils, bg_blender, ui_panels +from blenderkit import paths, utils, bg_blender, ui_panels, icons, tasks_queue, download import tempfile, os, subprocess, json, sys import bpy +from bpy.props import ( + FloatProperty, + IntProperty, + EnumProperty, + BoolProperty, + StringProperty, +) BLENDERKIT_EXPORT_DATA_FILE = "data.json" +thumbnail_resolutions = ( + ('256', '256', ''), + ('512', '512', ''), + ('1024', '1024 - minimum for public', ''), + ('2048', '2048', ''), +) + +thumbnail_angles = ( + ('DEFAULT', 'default', ''), + ('FRONT', 'front', ''), + ('SIDE', 'side', ''), + ('TOP', 'top', ''), +) + +thumbnail_snap = ( + ('GROUND', 'ground', ''), + ('WALL', 'wall', ''), + ('CEILING', 'ceiling', ''), + ('FLOAT', 'floating', ''), +) + + +def get_texture_ui(tpath, iname): + tex = bpy.data.textures.get(iname) + + if tpath.startswith('//'): + tpath = bpy.path.abspath(tpath) + + if not tex or not tex.image or not tex.image.filepath == tpath: + tasks_queue.add_task((utils.get_hidden_image, (tpath, iname)), only_last=True) + tasks_queue.add_task((utils.get_hidden_texture, (iname,)), only_last=True) + return None + return tex + def check_thumbnail(props, imgpath): img = utils.get_hidden_image(imgpath, 'upload_preview', force_reload=True) + print(' check thumbnail ', img) if img is not None: # and img.size[0] == img.size[1] and img.size[0] >= 512 and ( # img.file_format == 'JPEG' or img.file_format == 'PNG'): props.has_thumbnail = True props.thumbnail_generating_state = '' - return + + tex = utils.get_hidden_texture(img.name) + # pcoll = icons.icon_collections["previews"] + # pcoll.load(img.name, img.filepath, 'IMAGE') + + return img else: props.has_thumbnail = False output = '' @@ -55,7 +101,7 @@ def update_upload_model_preview(self, context): if ob is not None: props = ob.blenderkit imgpath = props.thumbnail - check_thumbnail(props, imgpath) + img = check_thumbnail(props, imgpath) def update_upload_scene_preview(self, context): @@ -83,55 +129,19 @@ def update_upload_brush_preview(self, context): check_thumbnail(props, imgpath) -def start_thumbnailer(self, context): +def start_thumbnailer(self=None, json_args=None, props=None, wait=False, add_bg_process=True): # Prepare to save the file - mainmodel = utils.get_active_model() - mainmodel.blenderkit.is_generating_thumbnail = True - mainmodel.blenderkit.thumbnail_generating_state = 'starting blender instance' binary_path = bpy.app.binary_path script_path = os.path.dirname(os.path.realpath(__file__)) - basename, ext = os.path.splitext(bpy.data.filepath) - if not basename: - basename = os.path.join(basename, "temp") - if not ext: - ext = ".blend" - asset_name = mainmodel.name - tempdir = tempfile.mkdtemp() - - file_dir = os.path.dirname(bpy.data.filepath) - thumb_path = os.path.join(file_dir, asset_name) - rel_thumb_path = os.path.join('//', asset_name) - - i = 0 - while os.path.isfile(thumb_path + '.jpg'): - thumb_path = os.path.join(file_dir, asset_name + '_' + str(i).zfill(4)) - rel_thumb_path = os.path.join('//', asset_name + '_' + str(i).zfill(4)) - i += 1 - - filepath = os.path.join(tempdir, "thumbnailer_blenderkit" + ext) + + ext = '.blend' + tfpath = paths.get_thumbnailer_filepath() - datafile = os.path.join(tempdir, BLENDERKIT_EXPORT_DATA_FILE) + datafile = os.path.join(json_args['tempdir'], BLENDERKIT_EXPORT_DATA_FILE) try: - # save a copy of actual scene but don't interfere with the users models - bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=False, copy=True) - - obs = utils.get_hierarchy(mainmodel) - obnames = [] - for ob in obs: - obnames.append(ob.name) - with open(datafile, 'w', encoding = 'utf-8') as s: - bkit = mainmodel.blenderkit - json.dump({ - "type": "model", - "models": str(obnames), - "thumbnail_angle": bkit.thumbnail_angle, - "thumbnail_snap_to": bkit.thumbnail_snap_to, - "thumbnail_background_lightness": bkit.thumbnail_background_lightness, - "thumbnail_resolution": bkit.thumbnail_resolution, - "thumbnail_samples": bkit.thumbnail_samples, - "thumbnail_denoising": bkit.thumbnail_denoising, - }, s, ensure_ascii=False, indent=4) + with open(datafile, 'w', encoding='utf-8') as s: + json.dump(json_args, s, ensure_ascii=False, indent=4) proc = subprocess.Popen([ binary_path, @@ -139,72 +149,49 @@ def start_thumbnailer(self, context): "-noaudio", tfpath, "--python", os.path.join(script_path, "autothumb_model_bg.py"), - "--", datafile, filepath, thumb_path, tempdir + "--", datafile, ], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=utils.get_process_flags()) - eval_path_computing = "bpy.data.objects['%s'].blenderkit.is_generating_thumbnail" % mainmodel.name - eval_path_state = "bpy.data.objects['%s'].blenderkit.thumbnail_generating_state" % mainmodel.name - eval_path = "bpy.data.objects['%s']" % mainmodel.name + eval_path_computing = "bpy.data.objects['%s'].blenderkit.is_generating_thumbnail" % json_args['asset_name'] + eval_path_state = "bpy.data.objects['%s'].blenderkit.thumbnail_generating_state" % json_args['asset_name'] + eval_path = "bpy.data.objects['%s']" % json_args['asset_name'] bg_blender.add_bg_process(eval_path_computing=eval_path_computing, eval_path_state=eval_path_state, eval_path=eval_path, process_type='THUMBNAILER', process=proc) - mainmodel.blenderkit.thumbnail = rel_thumb_path + '.jpg' - mainmodel.blenderkit.thumbnail_generating_state = 'Saving .blend file' except Exception as e: self.report({'WARNING'}, "Error while exporting file: %s" % str(e)) return {'FINISHED'} -def start_material_thumbnailer(self, context, wait=False): - # Prepare to save the file - mat = bpy.context.active_object.active_material - mat.blenderkit.is_generating_thumbnail = True - mat.blenderkit.thumbnail_generating_state = 'starting blender instance' +def start_material_thumbnailer(self=None, json_args=None, props=None, wait=False, add_bg_process=True): + ''' + + Parameters + ---------- + self + json_args - all arguments: + props - blenderkit upload props with thumbnail settings, to communicate back, if not present, not used. + wait - wait for the rendering to finish + + Returns + ------- + + ''' + if props: + props.is_generating_thumbnail = True + props.thumbnail_generating_state = 'starting blender instance' binary_path = bpy.app.binary_path script_path = os.path.dirname(os.path.realpath(__file__)) - basename, ext = os.path.splitext(bpy.data.filepath) - if not basename: - basename = os.path.join(basename, "temp") - if not ext: - ext = ".blend" - asset_name = mat.name - tempdir = tempfile.mkdtemp() - - file_dir = os.path.dirname(bpy.data.filepath) - - thumb_path = os.path.join(file_dir, asset_name) - rel_thumb_path = os.path.join('//', mat.name) - i = 0 - while os.path.isfile(thumb_path + '.png'): - thumb_path = os.path.join(file_dir, mat.name + '_' + str(i).zfill(4)) - rel_thumb_path = os.path.join('//', mat.name + '_' + str(i).zfill(4)) - i += 1 - - filepath = os.path.join(tempdir, "material_thumbnailer_cycles" + ext) + tfpath = paths.get_material_thumbnailer_filepath() - datafile = os.path.join(tempdir, BLENDERKIT_EXPORT_DATA_FILE) - try: - # save a copy of actual scene but don't interfere with the users models - bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=False, copy=True) + datafile = os.path.join(json_args['tempdir'], BLENDERKIT_EXPORT_DATA_FILE) - with open(datafile, 'w', encoding = 'utf-8') as s: - bkit = mat.blenderkit - json.dump({ - "type": "material", - "material": mat.name, - "thumbnail_type": bkit.thumbnail_generator_type, - "thumbnail_scale": bkit.thumbnail_scale, - "thumbnail_background": bkit.thumbnail_background, - "thumbnail_background_lightness": bkit.thumbnail_background_lightness, - "thumbnail_resolution": bkit.thumbnail_resolution, - "thumbnail_samples": bkit.thumbnail_samples, - "thumbnail_denoising": bkit.thumbnail_denoising, - "adaptive_subdivision": bkit.adaptive_subdivision, - "texture_size_meters": bkit.texture_size_meters, - }, s, ensure_ascii=False, indent=4) + try: + with open(datafile, 'w', encoding='utf-8') as s: + json.dump(json_args, s, ensure_ascii=False, indent=4) proc = subprocess.Popen([ binary_path, @@ -212,20 +199,28 @@ def start_material_thumbnailer(self, context, wait=False): "-noaudio", tfpath, "--python", os.path.join(script_path, "autothumb_material_bg.py"), - "--", datafile, filepath, thumb_path, tempdir + "--", datafile, ], bufsize=1, stdout=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=utils.get_process_flags()) - eval_path_computing = "bpy.data.materials['%s'].blenderkit.is_generating_thumbnail" % mat.name - eval_path_state = "bpy.data.materials['%s'].blenderkit.thumbnail_generating_state" % mat.name - eval_path = "bpy.data.materials['%s']" % mat.name + eval_path_computing = "bpy.data.materials['%s'].blenderkit.is_generating_thumbnail" % json_args['asset_name'] + eval_path_state = "bpy.data.materials['%s'].blenderkit.thumbnail_generating_state" % json_args['asset_name'] + eval_path = "bpy.data.materials['%s']" % json_args['asset_name'] - bg_blender.add_bg_process(eval_path_computing=eval_path_computing, eval_path_state=eval_path_state, + bg_blender.add_bg_process(name=json_args['asset_name'], eval_path_computing=eval_path_computing, + eval_path_state=eval_path_state, eval_path=eval_path, process_type='THUMBNAILER', process=proc) + if props: + props.thumbnail_generating_state = 'Saving .blend file' - mat.blenderkit.thumbnail = rel_thumb_path + '.png' - mat.blenderkit.thumbnail_generating_state = 'Saving .blend file' + if wait: + while proc.poll() is None: + stdout_data, stderr_data = proc.communicate() + print(stdout_data) except Exception as e: - self.report({'WARNING'}, "Error while packing file: %s" % str(e)) + if self: + self.report({'WARNING'}, "Error while packing file: %s" % str(e)) + else: + print(e) return {'FINISHED'} @@ -256,24 +251,198 @@ class GenerateThumbnailOperator(bpy.types.Operator): layout.prop(preferences, "thumbnail_use_gpu") def execute(self, context): - start_thumbnailer(self, context) + asset = utils.get_active_model() + asset.blenderkit.is_generating_thumbnail = True + asset.blenderkit.thumbnail_generating_state = 'starting blender instance' + + tempdir = tempfile.mkdtemp() + ext = '.blend' + filepath = os.path.join(tempdir, "thumbnailer_blenderkit" + ext) + + path_can_be_relative = True + file_dir = os.path.dirname(bpy.data.filepath) + if file_dir == '': + file_dir = tempdir + path_can_be_relative = False + + an_slug = paths.slugify(asset.name) + thumb_path = os.path.join(file_dir, an_slug) + if path_can_be_relative: + rel_thumb_path = os.path.join('//', an_slug) + else: + rel_thumb_path = thumb_path + + + i = 0 + while os.path.isfile(thumb_path + '.jpg'): + thumb_path = os.path.join(file_dir, an_slug + '_' + str(i).zfill(4)) + rel_thumb_path = os.path.join('//', an_slug + '_' + str(i).zfill(4)) + i += 1 + bkit = asset.blenderkit + + bkit.thumbnail = rel_thumb_path + '.jpg' + bkit.thumbnail_generating_state = 'Saving .blend file' + + # save a copy of actual scene but don't interfere with the users models + bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=False, copy=True) + # get all included objects + obs = utils.get_hierarchy(asset) + obnames = [] + for ob in obs: + obnames.append(ob.name) + + args_dict = { + "type": "material", + "asset_name": asset.name, + "filepath": filepath, + "thumbnail_path": thumb_path, + "tempdir": tempdir, + } + thumbnail_args = { + "type": "model", + "models": str(obnames), + "thumbnail_angle": bkit.thumbnail_angle, + "thumbnail_snap_to": bkit.thumbnail_snap_to, + "thumbnail_background_lightness": bkit.thumbnail_background_lightness, + "thumbnail_resolution": bkit.thumbnail_resolution, + "thumbnail_samples": bkit.thumbnail_samples, + "thumbnail_denoising": bkit.thumbnail_denoising, + } + args_dict.update(thumbnail_args) + + start_thumbnailer(self, + json_args=args_dict, + props=asset.blenderkit, wait=False) return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager - if bpy.data.filepath == '': - ui_panels.ui_message( - title = "Can't render thumbnail", - message = "please save your file first") + # if bpy.data.filepath == '': + # ui_panels.ui_message( + # title="Can't render thumbnail", + # message="please save your file first") + # + # return {'FINISHED'} + + return wm.invoke_props_dialog(self) + + +class ReGenerateThumbnailOperator(bpy.types.Operator): + """Generate Cycles thumbnail for model assets""" + bl_idname = "object.blenderkit_regenerate_thumbnail" + bl_label = "BlenderKit Thumbnail Re-generate" + bl_options = {'REGISTER', 'INTERNAL'} + + asset_index: IntProperty(name="Asset Index", description='asset index in search results', default=-1) + + thumbnail_background_lightness: FloatProperty(name="Thumbnail Background Lightness", + description="set to make your material stand out", default=1.0, + min=0.01, max=10) + + thumbnail_angle: EnumProperty( + name='Thumbnail Angle', + items=thumbnail_angles, + default='DEFAULT', + description='thumbnailer angle', + ) + + thumbnail_snap_to: EnumProperty( + name='Model Snaps To:', + items=thumbnail_snap, + default='GROUND', + description='typical placing of the interior. Leave on ground for most objects that respect gravity :)', + ) + + thumbnail_resolution: EnumProperty( + name="Resolution", + items=thumbnail_resolutions, + description="Thumbnail resolution", + default="1024", + ) + + thumbnail_samples: IntProperty(name="Cycles Samples", + description="cycles samples setting", default=100, + min=5, max=5000) + thumbnail_denoising: BoolProperty(name="Use Denoising", + description="Use denoising", default=True) + + @classmethod + def poll(cls, context): + return True # bpy.context.view_layer.objects.active is not None + + def draw(self, context): + ob = bpy.context.active_object + while ob.parent is not None: + ob = ob.parent + props = self + layout = self.layout + layout.label(text='thumbnailer settings') + layout.prop(props, 'thumbnail_background_lightness') + layout.prop(props, 'thumbnail_angle') + layout.prop(props, 'thumbnail_snap_to') + layout.prop(props, 'thumbnail_samples') + layout.prop(props, 'thumbnail_resolution') + layout.prop(props, 'thumbnail_denoising') + preferences = bpy.context.preferences.addons['blenderkit'].preferences + layout.prop(preferences, "thumbnail_use_gpu") - return {'FINISHED'} + def execute(self, context): + if not self.asset_index > -1: + return {'CANCELLED'} + + # either get the data from search results + sr = bpy.context.window_manager['search results'] + asset_data = sr[self.asset_index].to_dict() + + tempdir = tempfile.mkdtemp() + + an_slug = paths.slugify(asset_data['name']) + thumb_path = os.path.join(tempdir, an_slug) + + + args_dict = { + "type": "material", + "asset_name": asset_data['name'], + "asset_data": asset_data, + # "filepath": filepath, + "thumbnail_path": thumb_path, + "tempdir": tempdir, + "do_download": True, + "upload_after_render": True, + } + thumbnail_args = { + "type": "model", + "thumbnail_angle": self.thumbnail_angle, + "thumbnail_snap_to": self.thumbnail_snap_to, + "thumbnail_background_lightness": self.thumbnail_background_lightness, + "thumbnail_resolution": self.thumbnail_resolution, + "thumbnail_samples": self.thumbnail_samples, + "thumbnail_denoising": self.thumbnail_denoising, + } + args_dict.update(thumbnail_args) + + start_thumbnailer(self, + json_args=args_dict, + wait=False) + return {'FINISHED'} + start_thumbnailer(self, context) + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + # if bpy.data.filepath == '': + # ui_panels.ui_message( + # title="Can't render thumbnail", + # message="please save your file first") + # + # return {'FINISHED'} return wm.invoke_props_dialog(self) class GenerateMaterialThumbnailOperator(bpy.types.Operator): """Generate default thumbnail with Cycles renderer.""" - bl_idname = "object.blenderkit_material_thumbnail" + bl_idname = "object.blenderkit_generate_material_thumbnail" bl_label = "BlenderKit Material Thumbnail Generator" bl_options = {'REGISTER', 'INTERNAL'} @@ -300,7 +469,48 @@ class GenerateMaterialThumbnailOperator(bpy.types.Operator): layout.prop(preferences, "thumbnail_use_gpu") def execute(self, context): - start_material_thumbnailer(self, context) + asset = bpy.context.active_object.active_material + tempdir = tempfile.mkdtemp() + filepath = os.path.join(tempdir, "material_thumbnailer_cycles.blend") + # save a copy of actual scene but don't interfere with the users models + bpy.ops.wm.save_as_mainfile(filepath=filepath, compress=False, copy=True) + + thumb_dir = os.path.dirname(bpy.data.filepath) + thumb_path = os.path.join(thumb_dir, asset.name) + rel_thumb_path = os.path.join('//', asset.name) + # auto increase number of the generated thumbnail. + i = 0 + while os.path.isfile(thumb_path + '.png'): + thumb_path = os.path.join(thumb_dir, asset.name + '_' + str(i).zfill(4)) + rel_thumb_path = os.path.join('//', asset.name + '_' + str(i).zfill(4)) + i += 1 + + asset.blenderkit.thumbnail = rel_thumb_path + '.png' + bkit = asset.blenderkit + + args_dict = { + "type": "material", + "asset_name": asset.name, + "filepath": filepath, + "thumbnail_path": thumb_path, + "tempdir": tempdir, + } + + thumbnail_args = { + "thumbnail_type": bkit.thumbnail_generator_type, + "thumbnail_scale": bkit.thumbnail_scale, + "thumbnail_background": bkit.thumbnail_background, + "thumbnail_background_lightness": bkit.thumbnail_background_lightness, + "thumbnail_resolution": bkit.thumbnail_resolution, + "thumbnail_samples": bkit.thumbnail_samples, + "thumbnail_denoising": bkit.thumbnail_denoising, + "adaptive_subdivision": bkit.adaptive_subdivision, + "texture_size_meters": bkit.texture_size_meters, + } + args_dict.update(thumbnail_args) + start_material_thumbnailer(self, + json_args=args_dict, + props=asset.blenderkit, wait=False) return {'FINISHED'} @@ -309,11 +519,144 @@ class GenerateMaterialThumbnailOperator(bpy.types.Operator): return wm.invoke_props_dialog(self) +class ReGenerateMaterialThumbnailOperator(bpy.types.Operator): + """ + Generate default thumbnail with Cycles renderer. + Works also for assets from search results, without being downloaded before. + """ + bl_idname = "object.blenderkit_regenerate_material_thumbnail" + bl_label = "BlenderKit Material Thumbnail Re-Generator" + bl_options = {'REGISTER', 'INTERNAL'} + + asset_index: IntProperty(name="Asset Index", description='asset index in search results', default=-1) + + thumbnail_scale: FloatProperty(name="Thumbnail Object Size", + description="Size of material preview object in meters." + "Change for materials that look better at sizes different than 1m", + default=1, min=0.00001, max=10) + thumbnail_background: BoolProperty(name="Thumbnail Background (for Glass only)", + description="For refractive materials, you might need a background.\n" + "Don't use for other types of materials.\n" + "Transparent background is preferred", + default=False) + thumbnail_background_lightness: FloatProperty(name="Thumbnail Background Lightness", + description="Set to make your material stand out with enough contrast", + default=.9, + min=0.00001, max=1) + thumbnail_samples: IntProperty(name="Cycles Samples", + description="Cycles samples", default=100, + min=5, max=5000) + thumbnail_denoising: BoolProperty(name="Use Denoising", + description="Use denoising", default=True) + adaptive_subdivision: BoolProperty(name="Adaptive Subdivide", + description="Use adaptive displacement subdivision", default=False) + + thumbnail_resolution: EnumProperty( + name="Resolution", + items=thumbnail_resolutions, + description="Thumbnail resolution", + default="1024", + ) + + thumbnail_generator_type: EnumProperty( + name="Thumbnail Style", + items=( + ('BALL', 'Ball', ""), + ('BALL_COMPLEX', 'Ball complex', 'Complex ball to highlight edgewear or material thickness'), + ('FLUID', 'Fluid', 'Fluid'), + ('CLOTH', 'Cloth', 'Cloth'), + ('HAIR', 'Hair', 'Hair ') + ), + description="Style of asset", + default="BALL", + ) + + @classmethod + def poll(cls, context): + return True # bpy.context.view_layer.objects.active is not None + + def check(self, context): + return True + + def draw(self, context): + layout = self.layout + props = self + layout.prop(props, 'thumbnail_generator_type') + layout.prop(props, 'thumbnail_scale') + layout.prop(props, 'thumbnail_background') + if props.thumbnail_background: + layout.prop(props, 'thumbnail_background_lightness') + layout.prop(props, 'thumbnail_resolution') + layout.prop(props, 'thumbnail_samples') + layout.prop(props, 'thumbnail_denoising') + layout.prop(props, 'adaptive_subdivision') + preferences = bpy.context.preferences.addons['blenderkit'].preferences + layout.prop(preferences, "thumbnail_use_gpu") + + def execute(self, context): + + if not self.asset_index > -1: + return {'CANCELLED'} + + # either get the data from search results + sr = bpy.context.window_manager['search results'] + asset_data = sr[self.asset_index].to_dict() + + tempdir = tempfile.mkdtemp() + + thumb_path = os.path.join(tempdir, asset_data['name']) + + args_dict = { + "type": "material", + "asset_name": asset_data['name'], + "asset_data": asset_data, + "thumbnail_path": thumb_path, + "tempdir": tempdir, + "do_download": True, + "upload_after_render": True, + } + thumbnail_args = { + "thumbnail_type": self.thumbnail_generator_type, + "thumbnail_scale": self.thumbnail_scale, + "thumbnail_background": self.thumbnail_background, + "thumbnail_background_lightness": self.thumbnail_background_lightness, + "thumbnail_resolution": self.thumbnail_resolution, + "thumbnail_samples": self.thumbnail_samples, + "thumbnail_denoising": self.thumbnail_denoising, + "adaptive_subdivision": self.adaptive_subdivision, + "texture_size_meters": utils.get_param(asset_data, 'textureSizeMeters', 1.0), + } + args_dict.update(thumbnail_args) + start_material_thumbnailer(self, + json_args=args_dict, + wait=False) + + return {'FINISHED'} + + def invoke(self, context, event): + # scene = bpy.context.scene + # ui_props = scene.blenderkitUI + # if ui_props.active_index > -1: + # sr = bpy.context.window_manager['search results'] + # self.asset_data = dict(sr[ui_props.active_index]) + # else: + # + # active_asset = utils.get_active_asset_by_type(asset_type = self.asset_type) + # self.asset_data = active_asset.get('asset_data') + + wm = context.window_manager + return wm.invoke_props_dialog(self) + + def register_thumbnailer(): bpy.utils.register_class(GenerateThumbnailOperator) + bpy.utils.register_class(ReGenerateThumbnailOperator) bpy.utils.register_class(GenerateMaterialThumbnailOperator) + bpy.utils.register_class(ReGenerateMaterialThumbnailOperator) def unregister_thumbnailer(): bpy.utils.unregister_class(GenerateThumbnailOperator) + bpy.utils.unregister_class(ReGenerateThumbnailOperator) bpy.utils.unregister_class(GenerateMaterialThumbnailOperator) + bpy.utils.unregister_class(ReGenerateMaterialThumbnailOperator) |