Files
blender-portable-repo/extensions/user_default/memsaver_personal/mesh_decimation.py
T
2026-03-17 14:30:01 -06:00

201 lines
7.1 KiB
Python

# 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