# ##### 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 ##### # This module is for analyzing the asset and filling the tags automatically. # 1 part of the module effectively fills tags for the assets, # the 2nd part finds possible problems in the asset. import bpy from . import utils RENDER_OBTYPES = ["MESH", "CURVE", "SURFACE", "METABALL", "TEXT"] _BLE_5_PLUS = bpy.app.version >= (5, 0, 0) def check_material(props, mat): e = bpy.context.scene.render.engine shaders = [] textures = [] props.texture_count = 0 props.node_count = 0 props.total_megapixels = 0 total_pixels = 0 props.is_procedural = True if e == "CYCLES": if mat.node_tree is not None: checknodes = mat.node_tree.nodes[:] while len(checknodes) > 0: n = checknodes.pop() props.node_count += 1 if n.type == "GROUP": # dive deeper here. checknodes.extend(n.node_tree.nodes) if ( len(n.outputs) == 1 and n.outputs[0].type == "SHADER" and n.type != "GROUP" ): if n.type not in shaders: shaders.append(n.type) if n.type == "TEX_IMAGE": if n.image is not None: mattype = "image based" props.is_procedural = False if n.image not in textures: textures.append(n.image) props.texture_count += 1 total_pixels += n.image.size[0] * n.image.size[1] maxres = max(n.image.size[0], n.image.size[1]) props.texture_resolution_max = max( props.texture_resolution_max, maxres ) minres = min(n.image.size[0], n.image.size[1]) if props.texture_resolution_min == 0: props.texture_resolution_min = minres else: props.texture_resolution_min = min( props.texture_resolution_min, minres ) props.total_megapixels = round(total_pixels / (1024 * 1024)) props.shaders = "" for s in shaders: if s.startswith("BSDF_"): s = s[5:] s = s.lower().replace("_", " ") props.shaders += s + ", " def check_render_engine(props, obs): ob = obs[0] m = None e = bpy.context.scene.render.engine mattype = None materials = [] shaders = [] textures = [] props.uv = False props.texture_count = 0 props.total_megapixels = 0 total_pixels = 0 props.node_count = 0 for ob in obs: # TODO , this is duplicated here for other engines, otherwise this should be more clever. for ms in ob.material_slots: if ms.material is not None: m = ms.material if m.name not in materials: materials.append(m.name) if ob.type == "MESH" and len(ob.data.uv_layers) > 0: props.uv = True if e == "BLENDER_RENDER": props.engine = "BLENDER_INTERNAL" elif e == "CYCLES": props.engine = "CYCLES" # TODO: Clean this up, it's a mess. for mname in materials: m = bpy.data.materials[mname] if m is not None and m.node_tree is not None: checknodes = m.node_tree.nodes[:] while len(checknodes) > 0: n = checknodes.pop() props.node_count += 1 if n.type == "GROUP": # dive deeper here. if n.node_tree is not None: checknodes.extend(n.node_tree.nodes) if ( len(n.outputs) == 1 and n.outputs[0].type == "SHADER" and n.type != "GROUP" ): if n.type not in shaders: shaders.append(n.type) if n.type == "TEX_IMAGE": if n.image is not None and n.image not in textures: props.is_procedural = False mattype = "image based" textures.append(n.image) props.texture_count += 1 total_pixels += n.image.size[0] * n.image.size[1] maxres = max(n.image.size[0], n.image.size[1]) props.texture_resolution_max = max( props.texture_resolution_max, maxres ) minres = min(n.image.size[0], n.image.size[1]) if props.texture_resolution_min == 0: props.texture_resolution_min = minres else: props.texture_resolution_min = min( props.texture_resolution_min, minres ) props.total_megapixels = round(total_pixels / (1024 * 1024)) # if mattype == None: # mattype = 'procedural' # tags['material type'] = mattype elif e == "BLENDER_GAME": props.engine = "BLENDER_GAME" # write to object properties. props.materials = "" props.shaders = "" for m in materials: props.materials += m + ", " for s in shaders: if s.startswith("BSDF_"): s = s[5:] s = s.lower() s = s.replace("_", " ") props.shaders += s + ", " """ ISSUE:https://github.com/BlenderKit/BlenderKit/issues/1251 #1258 Commenting this function out, some user has reported this function got executed and failed due to missing add-on in Blender 4.2. Even though it is not called from anywhere, Python somehow went in here. So we are just commenting it out. In order to enable the func: 1. add-on object_print3d_utils had some bug in it, needs to be checked if it was fixed (are there any other better add-on for it?) 2. add-on object_print3d_utils is no longer preinstalled in Blender 4.2+, needs to be installed from extensions.blender.org -> "3D-Print Toolbox" def check_printable(props, obs): if len(obs) != 1: return addon_name = "object_print3d_utils" was_enabled, _ = addon_utils.check(addon_name) addon_utils.enable(addon_name) from object_print3d_utils import operators as ops check_cls = ( ops.MESH_OT_print3d_check_solid, # ops.Print3DCheckSolid, ops.MESH_OT_print3d_check_intersections, # ops.Print3DCheckIntersections, ops.MESH_OT_print3d_check_degenerate, # ops.Print3DCheckDegenerate, ops.MESH_OT_print3d_check_distorted, # ops.Print3DCheckDistorted, ops.MESH_OT_print3d_check_thick, # ops.Print3DCheckThick, ops.MESH_OT_print3d_check_sharp, # ops.Print3DCheckSharp, ) info = [] for cls in check_cls: cls.main_check(obs[0], info) printable = True for item in info: passed = item[0].endswith(" 0") if not passed: printable = False props.printable_3d = printable if not was_enabled: addon_utils.disable(addon_name) """ def check_rig(props, obs): for ob in obs: if ob.type == "ARMATURE": props.rig = True def has_keyframes(obj): """Checks if object has animation data with keyframes. This function only checks for keyframes, may return false negatives for objects animated with constraints, drivers, etc. """ if obj.animation_data is None: return False a = obj.animation_data.action if a is None: return False # should work from at least Blender4.2+ if _BLE_5_PLUS: # combined fcurves ranges # check if start and end frames are different if a.curve_frame_range[0] != a.curve_frame_range[1]: return True else: # older Blender versions for c in a.fcurves: if len(c.keyframe_points) > 1: return True return False def check_anim(props, obs): animated = False for ob in obs: if has_keyframes(ob): animated = True break if animated: props.animated = True def check_meshprops(props, obs): """checks polycount, manifold, mesh parts (not implemented)""" face_count = 0 face_count_render = 0 tris = 0 quads = 0 ngons = 0 vertices_count = 0 edges_counts = {} manifold = True for ob in obs: if ob.type != "MESH" and ob.type != "CURVE": continue ob_eval = None if ob.type == "CURVE": # depsgraph = bpy.context.evaluated_depsgraph_get() # object_eval = ob.evaluated_get(depsgraph) mesh = ob.to_mesh() else: mesh = ob.data if mesh == None: # One-point CURVE, can happen sometimes #1318 continue fco = len(mesh.polygons) face_count += fco vertices_count += len(mesh.vertices) fcor = fco for f in mesh.polygons: # face sides counter if len(f.vertices) == 3: tris += 1 elif len(f.vertices) == 4: quads += 1 elif len(f.vertices) > 4: ngons += 1 # manifold counter for i, v in enumerate(f.vertices): v1 = f.vertices[i - 1] e = (min(v, v1), max(v, v1)) edges_counts[e] = edges_counts.get(e, 0) + 1 # all meshes have to be manifold for this to work. manifold = manifold and not any( i in edges_counts.values() for i in [0, 1, 3, 4] ) for m in ob.modifiers: if m.type == "SUBSURF" or m.type == "MULTIRES": fcor *= 4**m.render_levels if ( m.type == "SOLIDIFY" ): # this is rough estimate, not to waste time with evaluating all nonmanifold edges fcor *= 2 if m.type == "ARRAY": fcor *= m.count if m.type == "MIRROR": fcor *= 2 if m.type == "DECIMATE": fcor *= m.ratio face_count_render += fcor if ob_eval: ob_eval.to_mesh_clear() # write out props props.face_count = int(face_count) props.face_count_render = int(face_count_render) if quads > 0 and tris == 0 and ngons == 0: props.mesh_poly_type = "QUAD" elif quads > tris and quads > ngons: props.mesh_poly_type = "QUAD_DOMINANT" elif tris > quads and tris > quads: props.mesh_poly_type = "TRI_DOMINANT" elif quads == 0 and tris > 0 and ngons == 0: props.mesh_poly_type = "TRI" elif ngons > quads and ngons > tris: props.mesh_poly_type = "NGON" else: props.mesh_poly_type = "OTHER" props.manifold = manifold def countObs(props, obs): ob_types = {} count = len(obs) for ob in obs: otype = ob.type.lower() ob_types[otype] = ob_types.get(otype, 0) + 1 props.object_count = count def check_modifiers(props, obs): # modif_mapping = { # } modifiers = [] for ob in obs: for m in ob.modifiers: mtype = m.type mtype = mtype.replace("_", " ") mtype = mtype.lower() # mtype = mtype.capitalize() if mtype not in modifiers: modifiers.append(mtype) if m.type == "SMOKE": if m.smoke_type == "FLOW": smt = m.flow_settings.smoke_flow_type if smt == "BOTH" or smt == "FIRE": modifiers.append("fire") # for mt in modifiers: effectmodifiers = [ "soft body", "fluid simulation", "particle system", "collision", "smoke", "cloth", "dynamic paint", ] for m in modifiers: if m in effectmodifiers: props.simulation = True if ob.rigid_body is not None: props.simulation = True modifiers.append("rigid body") finalstr = "" for m in modifiers: finalstr += m finalstr += "," props.modifiers = finalstr def get_autotags(): """call all analysis functions""" ui = bpy.context.window_manager.blenderkitUI if ui.asset_type == "MODEL" or ui.asset_type == "PRINTABLE": ob = utils.get_active_model() obs = utils.get_hierarchy(ob) props = ob.blenderkit if props.name == "": props.name = ob.name # reset some properties here, because they might not get re-filled at all when they aren't needed anymore. props.texture_resolution_max = 0 props.texture_resolution_min = 0 # disabled printing checking, some 3d print addon bug. # bug fixed, could be enabled in the future # also disable because add-on is not installed in Blender 4.2+, has to be installed from extensions.blender.org # check the commented out function for more details # check_printable( props, obs) check_render_engine(props, obs) dim, bbox_min, bbox_max = utils.get_dimensions(obs) props.dimensions = dim props.bbox_min = bbox_min props.bbox_max = bbox_max check_rig(props, obs) check_anim(props, obs) check_meshprops(props, obs) check_modifiers(props, obs) countObs(props, obs) elif ui.asset_type == "MATERIAL": # reset some properties here, because they might not get re-filled at all when they aren't needed anymore. mat = utils.get_active_asset() props = mat.blenderkit props.texture_resolution_max = 0 props.texture_resolution_min = 0 check_material(props, mat) elif ui.asset_type == "HDR": # reset some properties here, because they might not get re-filled at all when they aren't needed anymore. hdr = utils.get_active_asset() props = hdr.blenderkit props.texture_resolution_max = max(hdr.size[0], hdr.size[1]) class AutoFillTags(bpy.types.Operator): """Fill tags for asset. Now run before upload, no need to interact from user side""" bl_idname = "object.blenderkit_auto_tags" bl_label = "Generate Auto Tags for BlenderKit" bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): return utils.uploadable_asset_poll() def execute(self, context): get_autotags() return {"FINISHED"} def register_asset_inspector(): bpy.utils.register_class(AutoFillTags) def unregister_asset_inspector(): bpy.utils.unregister_class(AutoFillTags) if __name__ == "__main__": register() # type: ignore # TODO: fix call to missing function