# copyright (c) 2018- polygoniq xyz s.r.o. import typing import collections import bpy import logging from . import object_render_estimator logger = logging.getLogger(f"polygoniq.{__name__}") DECIMATE_MODIFIER_NAME = "memsaver_decimate" def get_meshes_used_in_object(obj: bpy.types.Object) -> typing.Iterable[bpy.types.Mesh]: if obj.type == 'EMPTY': if obj.instance_type == 'COLLECTION': if obj.instance_collection is None: return # warn? for instanced_object in obj.instance_collection.objects: yield from get_meshes_used_in_object(instanced_object) elif obj.type == 'MESH': if obj.data is None: return # warn? yield obj.data def generate_mesh_objects_map( objects: typing.Iterable[bpy.types.Object], ) -> typing.DefaultDict[bpy.types.Mesh, typing.List[bpy.types.Object]]: mesh_objects_map: typing.DefaultDict[bpy.types.Mesh, typing.List[bpy.types.Object]] = ( collections.defaultdict(list) ) for obj in objects: meshes = get_meshes_used_in_object(obj) for mesh in meshes: mesh_objects_map[mesh].append(obj) return mesh_objects_map def update_object_decimation_ratio_map( objects_decimation_ratio_map: typing.DefaultDict[bpy.types.Object, float], mesh_objects_map: typing.DefaultDict[bpy.types.Mesh, typing.List[bpy.types.Object]], scene: bpy.types.Scene, camera: bpy.types.Object, objects: typing.Iterable[bpy.types.Object], full_quality_distance: float, lowest_quality_distance: float, lowest_quality_decimation_ratio: float, lowest_face_count: float, ) -> None: decimation_interpolation_denominator = lowest_quality_distance - full_quality_distance if decimation_interpolation_denominator <= 0: decimation_interpolation_denominator = 1 for mesh, objects in mesh_objects_map.items(): if len(mesh.polygons) < lowest_face_count: continue # ignore meshes with lower face count than the one given mesh_min_distance = float("inf") for obj in objects: if obj.library is not None: # linked, non-editable object # set min distance to 0 to avoid any decimation, we won't be able to add the same # decimation to all objects using this mesh, so let's not decimate at all mesh_min_distance = 0.0 break _, _, obj_min_distance = object_render_estimator.get_object_2d_bounds( scene, camera, obj ) if obj_min_distance is not None: mesh_min_distance = min(mesh_min_distance, obj_min_distance) # object_render_estimator will return negative values of the object intersects the camera # we assume non-negative values so let's clamp to 0 mesh_min_distance = max(0.0, mesh_min_distance) # Everything closer than full_quality_distance has decimation ratio 1.0 # Everything further than lowest_quality_distance has decimation ratio set to # lowest_quality_decimation_ratio # Between minimum_distance and lowest_quality_distance interpolate linearly from 1.0 to # lowest_quality_decimation_ratio decimation_ratio = 1.0 if mesh_min_distance <= full_quality_distance: decimation_ratio = 1.0 elif mesh_min_distance <= lowest_quality_distance: nominator = mesh_min_distance - full_quality_distance interpolation_factor = nominator / decimation_interpolation_denominator assert interpolation_factor >= 0.0 assert interpolation_factor <= 1.0 decimation_ratio = ( 1.0 - interpolation_factor ) * 1.0 + interpolation_factor * lowest_quality_decimation_ratio else: decimation_ratio = lowest_quality_decimation_ratio assert decimation_ratio >= 0.0 assert decimation_ratio <= 1.0 for obj in objects: objects_decimation_ratio_map[obj] = max( objects_decimation_ratio_map[obj], decimation_ratio ) def get_objects_decimation_ratio_map_current_frame( scene: bpy.types.Scene, camera: bpy.types.Object, objects: typing.Iterable[bpy.types.Object], minimum_distance: float, lowest_quality_distance: float, lowest_quality_decimation_ratio: float, lowest_face_count: float, ) -> typing.DefaultDict[bpy.types.Object, float]: mesh_objects_map = generate_mesh_objects_map(objects) objects_decimation_ratio_map: typing.DefaultDict[bpy.types.Object, float] = ( collections.defaultdict(float) ) update_object_decimation_ratio_map( objects_decimation_ratio_map, mesh_objects_map, scene, camera, objects, minimum_distance, lowest_quality_distance, lowest_quality_decimation_ratio, lowest_face_count, ) return objects_decimation_ratio_map def get_objects_decimation_ratio_map_animation_mode( scene: bpy.types.Scene, camera: bpy.types.Object, objects: typing.Iterable[bpy.types.Object], minimum_distance: float, lowest_quality_distance: float, lowest_quality_decimation_ratio: float, lowest_face_count: float, ) -> typing.DefaultDict[bpy.types.Object, float]: previous_frame_current = scene.frame_current try: mesh_objects_map = generate_mesh_objects_map(objects) objects_decimation_ratio_map: typing.DefaultDict[bpy.types.Object, float] = ( collections.defaultdict(float) ) current_frame = scene.frame_start while current_frame <= scene.frame_end: scene.frame_current = current_frame bpy.context.view_layer.update() update_object_decimation_ratio_map( objects_decimation_ratio_map, mesh_objects_map, scene, camera, objects, minimum_distance, lowest_quality_distance, lowest_quality_decimation_ratio, lowest_face_count, ) current_frame += 1 return objects_decimation_ratio_map finally: scene.frame_current = previous_frame_current bpy.context.view_layer.update() def revert_to_original(obj: bpy.types.Object) -> None: if DECIMATE_MODIFIER_NAME in obj.modifiers: obj.modifiers.remove(obj.modifiers[DECIMATE_MODIFIER_NAME]) def set_decimation_ratio(obj: bpy.types.Object, decimation_ratio: float) -> None: if decimation_ratio == 1.0: # no decimation revert_to_original(obj) else: memsaver_decimation = None if DECIMATE_MODIFIER_NAME in obj.modifiers: memsaver_decimation = obj.modifiers[DECIMATE_MODIFIER_NAME] else: memsaver_decimation = obj.modifiers.new(DECIMATE_MODIFIER_NAME, 'DECIMATE') if memsaver_decimation is None: logger.error( f"Failed to create memsaver decimation modifier for object {obj.name} " f"of type {obj.type}, skipping!" ) return memsaver_decimation.ratio = decimation_ratio