feat(渲染): 新增线稿渲染功能

添加线稿渲染功能,用于生成 ControlNet Canny 所需的黑白线稿图。实现通过 Freestyle 渲染引擎,在白色背景上生成黑色线条,并临时替换材质与世界设置以确保输出符合要求。同时更新了清理函数以移除新增的临时白色材质。
This commit is contained in:
2026-02-13 16:51:46 +08:00
parent 803a6557f1
commit 05d8702373
2 changed files with 147 additions and 4 deletions

149
utils.py
View File

@@ -321,6 +321,145 @@ def _render_normal(operator):
return _render_with_material(operator, normal_mat, 'normal.png', '法向')
def _create_white_material():
"""Create a flat white emission material for line art background."""
mat_name = '__ComfyWhiteMat__'
if mat_name in bpy.data.materials:
return 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()
emission = nodes.new('ShaderNodeEmission')
emission.location = (0, 0)
emission.inputs['Color'].default_value = (1, 1, 1, 1)
emission.inputs['Strength'].default_value = 1.0
output = nodes.new('ShaderNodeOutputMaterial')
output.location = (200, 0)
links.new(emission.outputs['Emission'], output.inputs['Surface'])
return mat
def _render_lineart(operator):
"""Render line art using Freestyle (black lines on white) for ControlNet Canny."""
scene = bpy.context.scene
output_dir = _ensure_output_dir()
converted_obj = _get_converted_obj(scene, operator)
if converted_obj is None:
return False
# Save original state
vl = scene.view_layers["ViewLayer"]
orig_materials = [slot.material for slot in converted_obj.material_slots]
orig_use_freestyle = scene.render.use_freestyle
orig_vl_use_freestyle = vl.use_freestyle
orig_line_thickness = scene.render.line_thickness
orig_use_nodes = scene.use_nodes
orig_use_compositing = scene.render.use_compositing
orig_filepath = scene.render.filepath
orig_format = scene.render.image_settings.file_format
orig_color_mode = scene.render.image_settings.color_mode
orig_film_transparent = scene.render.film_transparent
orig_world = scene.world
# Apply white emission material to all slots
white_mat = _create_white_material()
if len(converted_obj.material_slots) == 0:
converted_obj.data.materials.append(white_mat)
else:
for i in range(len(converted_obj.material_slots)):
converted_obj.material_slots[i].material = white_mat
# Enable Freestyle
scene.render.use_freestyle = True
vl.use_freestyle = True
scene.render.line_thickness = 1.0
# Configure lineset: black lines, all relevant edge types
fs = vl.freestyle_settings
# Save original linesets state
orig_lineset_settings = []
for ls in fs.linesets:
orig_lineset_settings.append({
'color': tuple(ls.linestyle.color),
'thickness': ls.linestyle.thickness,
})
if len(fs.linesets) == 0:
ls = fs.linesets.new('LineSet')
else:
ls = fs.linesets[0]
ls.linestyle.color = (0, 0, 0) # Black lines
ls.linestyle.thickness = 1.5
ls.select_silhouette = True
ls.select_border = True
ls.select_crease = True
ls.select_edge_mark = True
# White world background
white_world = bpy.data.worlds.new('__ComfyWhiteWorld__')
white_world.use_nodes = True
bg_node = white_world.node_tree.nodes.get('Background')
if bg_node:
bg_node.inputs['Color'].default_value = (1, 1, 1, 1)
bg_node.inputs['Strength'].default_value = 1.0
scene.world = white_world
scene.render.film_transparent = False
# Disable compositor
scene.use_nodes = False
scene.render.use_compositing = False
# Setup output
out_path = os.path.join(output_dir, 'lineart.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 materials
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
# Restore lineset settings
for i, ls_data in enumerate(orig_lineset_settings):
if i < len(fs.linesets):
fs.linesets[i].linestyle.color = ls_data['color']
fs.linesets[i].linestyle.thickness = ls_data['thickness']
# Restore all settings
scene.render.use_freestyle = orig_use_freestyle
vl.use_freestyle = orig_vl_use_freestyle
scene.render.line_thickness = orig_line_thickness
scene.use_nodes = orig_use_nodes
scene.render.use_compositing = orig_use_compositing
scene.render.filepath = orig_filepath
scene.render.image_settings.file_format = orig_format
scene.render.image_settings.color_mode = orig_color_mode
scene.render.film_transparent = orig_film_transparent
scene.world = orig_world
# Remove temporary world
if '__ComfyWhiteWorld__' in bpy.data.worlds:
bpy.data.worlds.remove(bpy.data.worlds['__ComfyWhiteWorld__'])
_add_rendered_image(scene, '线稿', out_path)
return True
class Comfy_OT_Mvgen_B6229(bpy.types.Operator):
bl_idname = "comfyui.mvgen_b6229"
bl_label = "生成多视角视图"
@@ -384,6 +523,9 @@ class Comfy_OT_Mvgen_B6229(bpy.types.Operator):
# --- Render Normal Map ---
_render_normal(self)
# --- Render Line Art ---
_render_lineart(self)
return {"FINISHED"}
def invoke(self, context, _event):
@@ -466,9 +608,10 @@ class Comfy_OT_CleanupMvgen(bpy.types.Operator):
pcoll = _get_preview_collection()
pcoll.clear()
# Remove temp depth material
if '__ComfyDepthMat__' in bpy.data.materials:
bpy.data.materials.remove(bpy.data.materials['__ComfyDepthMat__'])
# Remove temp materials
for mat_name in ('__ComfyDepthMat__', '__ComfyWhiteMat__'):
if mat_name in bpy.data.materials:
bpy.data.materials.remove(bpy.data.materials[mat_name])
# Clear converted name
if '_comfy_converted_name' in scene: