refactor(渲染): 重构深度和法向渲染逻辑以提高可维护性

- 将深度渲染从依赖外部合成节点改为程序化材质生成
- 提取通用渲染逻辑到 `_render_with_material` 函数
- 改进转换对象追踪方式,使用场景属性存储名称
- 清理函数中移除临时深度材质和场景属性
This commit is contained in:
2026-02-13 13:23:55 +08:00
parent f5693147fe
commit 803a6557f1

202
utils.py
View File

@@ -177,35 +177,105 @@ def _add_rendered_image(scene, name, filepath):
scene.sna_rendered_images_index = len(scene.sna_rendered_images) - 1 scene.sna_rendered_images_index = len(scene.sna_rendered_images) - 1
def _render_depth(operator): def _create_depth_material(near, far):
"""Render depth map using Depth_Mat compositor NodeTree.""" """Create a material that renders depth as grayscale (near=white, far=black)."""
mat_name = '__ComfyDepthMat__'
if mat_name in bpy.data.materials:
bpy.data.materials.remove(bpy.data.materials[mat_name])
mat = bpy.data.materials.new(name=mat_name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
# Camera Data → View Z Depth
cam_data = nodes.new('ShaderNodeCameraData')
cam_data.location = (0, 0)
# Map Range: [near..far] → [1..0] (near=white, far=black)
map_range = nodes.new('ShaderNodeMapRange')
map_range.location = (200, 0)
map_range.inputs['From Min'].default_value = near
map_range.inputs['From Max'].default_value = far
map_range.inputs['To Min'].default_value = 1.0
map_range.inputs['To Max'].default_value = 0.0
map_range.clamp = True
# Emission shader (unlit)
emission = nodes.new('ShaderNodeEmission')
emission.location = (400, 0)
# Output
output = nodes.new('ShaderNodeOutputMaterial')
output.location = (600, 0)
links.new(cam_data.outputs['View Z Depth'], map_range.inputs['Value'])
links.new(map_range.outputs['Result'], emission.inputs['Color'])
links.new(emission.outputs['Emission'], output.inputs['Surface'])
return mat
def _get_converted_obj(scene, operator):
"""Find the converted copy object using saved name."""
converted_name = scene.get('_comfy_converted_name', '')
converted_obj = bpy.data.objects.get(converted_name)
if converted_obj is None or converted_obj.type != 'MESH':
operator.report({'ERROR'}, message='Cannot find converted copy: ' + converted_name)
return None
return converted_obj
def _get_depth_range(scene, converted_obj):
"""Calculate the min/max view-Z depth from the camera to the object bounding box."""
cam = scene.camera
if cam is None:
return 0.1, 10.0
cam_inv = cam.matrix_world.inverted()
min_z = float('inf')
max_z = float('-inf')
for corner in converted_obj.bound_box:
world_pt = converted_obj.matrix_world @ Vector(corner)
local_pt = cam_inv @ world_pt
z = -local_pt.z # View Z Depth is positive distance along camera -Z
min_z = min(min_z, z)
max_z = max(max_z, z)
# Add padding
padding = max(0.01, (max_z - min_z) * 0.1)
return max(0.001, min_z - padding), max_z + padding
def _render_with_material(operator, mat, out_name, label):
"""Generic render: apply material to converted copy, render, restore."""
scene = bpy.context.scene scene = bpy.context.scene
output_dir = _ensure_output_dir() output_dir = _ensure_output_dir()
# Force fresh Depth_Mat by removing any cached version converted_obj = _get_converted_obj(scene, operator)
if 'Depth_Mat' in bpy.data.node_groups: if converted_obj is None:
bpy.data.node_groups.remove(bpy.data.node_groups['Depth_Mat'])
# Append fresh Depth_Mat from Materials.blend
depth_tree = _append_from_materials_blend('NodeTree', 'Depth_Mat')
if depth_tree is None:
operator.report({'ERROR'}, message='Failed to load Depth_Mat from Materials.blend')
return False return False
# Save original render settings # Save original state
orig_materials = [slot.material for slot in converted_obj.material_slots]
orig_use_nodes = scene.use_nodes orig_use_nodes = scene.use_nodes
orig_comp_group = scene.compositing_node_group orig_use_compositing = scene.render.use_compositing
orig_filepath = scene.render.filepath orig_filepath = scene.render.filepath
orig_format = scene.render.image_settings.file_format orig_format = scene.render.image_settings.file_format
orig_color_mode = scene.render.image_settings.color_mode orig_color_mode = scene.render.image_settings.color_mode
# Enable compositor and assign Depth_Mat as the compositing node group # Replace ALL material slots with the render material
scene.use_nodes = True if len(converted_obj.material_slots) == 0:
scene.compositing_node_group = depth_tree converted_obj.data.materials.append(mat)
scene.view_layers["ViewLayer"].use_pass_z = True else:
for i in range(len(converted_obj.material_slots)):
converted_obj.material_slots[i].material = mat
# Fully disable compositor (both flags needed in Blender 5.0)
scene.use_nodes = False
scene.render.use_compositing = False
# Setup output # Setup output
out_path = os.path.join(output_dir, 'depth.png') out_path = os.path.join(output_dir, out_name)
scene.render.filepath = out_path scene.render.filepath = out_path
scene.render.image_settings.file_format = 'PNG' scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGB' scene.render.image_settings.color_mode = 'RGB'
@@ -213,73 +283,42 @@ def _render_depth(operator):
# Render # Render
bpy.ops.render.render(write_still=True) bpy.ops.render.render(write_still=True)
# Restore all settings # Restore original materials per slot
scene.compositing_node_group = orig_comp_group if len(orig_materials) == 0:
converted_obj.data.materials.clear()
else:
for i, mat_orig in enumerate(orig_materials):
if i < len(converted_obj.material_slots):
converted_obj.material_slots[i].material = mat_orig
scene.use_nodes = orig_use_nodes scene.use_nodes = orig_use_nodes
scene.render.use_compositing = orig_use_compositing
scene.render.filepath = orig_filepath scene.render.filepath = orig_filepath
scene.render.image_settings.file_format = orig_format scene.render.image_settings.file_format = orig_format
scene.render.image_settings.color_mode = orig_color_mode scene.render.image_settings.color_mode = orig_color_mode
# Add to rendered images list _add_rendered_image(scene, label, out_path)
_add_rendered_image(scene, 'depth', out_path)
return True return True
def _render_normal(operator): def _render_depth(operator):
"""Render normal map by applying Normal_Mat material to active object.""" """Render depth map using a programmatic depth material."""
scene = bpy.context.scene scene = bpy.context.scene
obj = scene.sna_activeobject converted_obj = _get_converted_obj(scene, operator)
output_dir = _ensure_output_dir() if converted_obj is None:
return False
# Append the Normal_Mat material near, far = _get_depth_range(scene, converted_obj)
depth_mat = _create_depth_material(near, far)
return _render_with_material(operator, depth_mat, 'depth.png', 'depth')
def _render_normal(operator):
"""Render normal map using Normal_Mat material."""
normal_mat = _append_from_materials_blend('Material', 'Normal_Mat') normal_mat = _append_from_materials_blend('Material', 'Normal_Mat')
if normal_mat is None: if normal_mat is None:
operator.report({'ERROR'}, message='Failed to load Normal_Mat from Materials.blend') operator.report({'ERROR'}, message='Failed to load Normal_Mat from Materials.blend')
return False return False
return _render_with_material(operator, normal_mat, 'normal.png', '法向')
# Find the converted object (the active object after convert)
converted_obj = bpy.context.view_layer.objects.active
if converted_obj is None or converted_obj.type != 'MESH':
converted_obj = obj
# Save original materials and render settings
orig_materials = [slot.material for slot in converted_obj.material_slots]
orig_use_nodes = scene.use_nodes
orig_filepath = scene.render.filepath
orig_format = scene.render.image_settings.file_format
orig_color_mode = scene.render.image_settings.color_mode
# Clear and assign Normal_Mat
converted_obj.data.materials.clear()
converted_obj.data.materials.append(normal_mat)
# Disable compositor for normal pass (direct render)
scene.use_nodes = False
# Setup output
out_path = os.path.join(output_dir, 'normal.png')
scene.render.filepath = out_path
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGB'
# Render
bpy.ops.render.render(write_still=True)
# Restore original materials and settings
converted_obj.data.materials.clear()
for mat in orig_materials:
converted_obj.data.materials.append(mat)
scene.use_nodes = orig_use_nodes
scene.render.filepath = orig_filepath
scene.render.image_settings.file_format = orig_format
scene.render.image_settings.color_mode = orig_color_mode
# Add to rendered images list
_add_rendered_image(scene, 'normal', out_path)
return True
class Comfy_OT_Mvgen_B6229(bpy.types.Operator): class Comfy_OT_Mvgen_B6229(bpy.types.Operator):
@@ -332,6 +371,8 @@ class Comfy_OT_Mvgen_B6229(bpy.types.Operator):
return {"CANCELLED"} return {"CANCELLED"}
bpy.context.scene.sna_activeobject.modifiers['MV_Nodes'].node_group = node_group bpy.context.scene.sna_activeobject.modifiers['MV_Nodes'].node_group = node_group
bpy.ops.object.convert(target='MESH', keep_original=True) bpy.ops.object.convert(target='MESH', keep_original=True)
# Save the converted copy name immediately (active object after convert)
bpy.context.scene['_comfy_converted_name'] = bpy.context.active_object.name
bpy.data.objects[bpy.context.scene.sna_activeobject_name].hide_render = True bpy.data.objects[bpy.context.scene.sna_activeobject_name].hide_render = True
bpy.ops.comfyui.aovbake_ad980('INVOKE_DEFAULT', ) bpy.ops.comfyui.aovbake_ad980('INVOKE_DEFAULT', )
bpy.data.objects[bpy.context.scene.sna_activeobject_name].modifiers.clear() bpy.data.objects[bpy.context.scene.sna_activeobject_name].modifiers.clear()
@@ -393,13 +434,10 @@ class Comfy_OT_CleanupMvgen(bpy.types.Operator):
def execute(self, context): def execute(self, context):
scene = context.scene scene = context.scene
# Delete the converted copy (active object that is NOT the original) # Delete the converted copy using saved name
orig_name = scene.sna_activeobject_name converted_name = scene.get('_comfy_converted_name', '')
if orig_name: if converted_name and converted_name in bpy.data.objects:
# The converted copy is typically named with .001 suffix bpy.data.objects.remove(bpy.data.objects[converted_name], do_unlink=True)
for obj in list(bpy.data.objects):
if obj.name != orig_name and obj.name.startswith(orig_name) and obj.type == 'MESH':
bpy.data.objects.remove(obj, do_unlink=True)
# Delete the bake camera # Delete the bake camera
bake_cam = scene.sna_bakecam bake_cam = scene.sna_bakecam
@@ -412,11 +450,11 @@ class Comfy_OT_CleanupMvgen(bpy.types.Operator):
bpy.data.objects.remove(bpy.data.objects['ObjectCamera'], do_unlink=True) bpy.data.objects.remove(bpy.data.objects['ObjectCamera'], do_unlink=True)
# Restore original object visibility # Restore original object visibility
orig_name = scene.sna_activeobject_name
if orig_name and orig_name in bpy.data.objects: if orig_name and orig_name in bpy.data.objects:
orig_obj = bpy.data.objects[orig_name] orig_obj = bpy.data.objects[orig_name]
orig_obj.hide_render = False orig_obj.hide_render = False
orig_obj.hide_set(False) orig_obj.hide_set(False)
# Make it active and selected
bpy.context.view_layer.objects.active = orig_obj bpy.context.view_layer.objects.active = orig_obj
orig_obj.select_set(True) orig_obj.select_set(True)
@@ -428,6 +466,14 @@ class Comfy_OT_CleanupMvgen(bpy.types.Operator):
pcoll = _get_preview_collection() pcoll = _get_preview_collection()
pcoll.clear() pcoll.clear()
# Remove temp depth material
if '__ComfyDepthMat__' in bpy.data.materials:
bpy.data.materials.remove(bpy.data.materials['__ComfyDepthMat__'])
# Clear converted name
if '_comfy_converted_name' in scene:
del scene['_comfy_converted_name']
return {'FINISHED'} return {'FINISHED'}