"""
Modifier tools for retopology workflows.
Provides safe, parameterized tools for adding and configuring Blender modifiers.
"""
from typing import Dict, Any, List, Optional
from mcp.server.fastmcp import Context
import logging
logger = logging.getLogger("BlenderMCPServer")
# ============================================================================
# MIRROR MODIFIER
# ============================================================================
def add_mirror_modifier(
ctx: Context,
blender_connection,
axis: str = "X",
use_clip: bool = True,
use_bisect: bool = True,
mirror_object: Optional[str] = None
) -> str:
"""
Add a Mirror modifier to the active mesh object.
Creates a Mirror modifier that mirrors the mesh along the specified axis
with optional clipping and bisecting.
Parameters:
- axis: Mirror axis - 'X', 'Y', or 'Z' (default: 'X')
- use_clip: Prevent vertices from crossing the mirror plane (default: true)
- use_bisect: Cut the mesh at the mirror plane (default: true)
- mirror_object: Name of object to use as mirror plane (optional)
Returns status message with modifier details.
"""
try:
if axis not in ['X', 'Y', 'Z']:
return f"Error: Invalid axis '{axis}'. Must be 'X', 'Y', or 'Z'."
result = blender_connection.send_command("add_mirror_modifier", {
"axis": axis,
"use_clip": use_clip,
"use_bisect": use_bisect,
"mirror_object": mirror_object
})
if "error" in result:
return f"Error: {result['error']}"
output = "Mirror Modifier Added!\n\n"
output += f"Mirror Axis: {axis}\n"
output += f"Clipping: {use_clip}\n"
output += f"Bisect: {use_bisect}\n"
if mirror_object:
output += f"Mirror Object: {mirror_object}\n"
return output
except Exception as e:
logger.error(f"Error adding mirror modifier: {str(e)}")
return f"Error adding mirror modifier: {str(e)}"
# ============================================================================
# SHRINKWRAP MODIFIER
# ============================================================================
def add_shrinkwrap_modifier(
ctx: Context,
blender_connection,
target: str,
method: str = "NEAREST_SURFACEPOINT",
offset: float = 0.0,
on_cage: bool = True,
cull_backfaces: bool = False
) -> str:
"""
Add a Shrinkwrap modifier to project low-poly mesh onto high-poly surface.
Projects vertices of the active object onto the target surface using the
specified method. Essential for retopology workflows.
Parameters:
- target: Name of the target object to shrinkwrap to
- method: Wrap method - 'NEAREST_SURFACEPOINT', 'PROJECT', 'NEAREST_VERTEX', 'TARGET_PROJECT' (default: 'NEAREST_SURFACEPOINT')
- offset: Distance to offset from target surface (default: 0.0)
- on_cage: Project in screen space for retopology (default: true)
- cull_backfaces: Don't project to backfaces (default: false)
Returns status message with modifier details.
"""
try:
valid_methods = ['NEAREST_SURFACEPOINT', 'PROJECT', 'NEAREST_VERTEX', 'TARGET_PROJECT']
if method not in valid_methods:
return f"Error: Invalid method '{method}'. Must be one of {valid_methods}."
result = blender_connection.send_command("add_shrinkwrap_modifier", {
"target": target,
"method": method,
"offset": offset,
"on_cage": on_cage,
"cull_backfaces": cull_backfaces
})
if "error" in result:
return f"Error: {result['error']}"
output = "Shrinkwrap Modifier Added!\n\n"
output += f"Target: {target}\n"
output += f"Method: {method}\n"
output += f"Offset: {offset}\n"
output += f"On Cage: {on_cage}\n"
output += f"Cull Backfaces: {cull_backfaces}\n"
return output
except Exception as e:
logger.error(f"Error adding shrinkwrap modifier: {str(e)}")
return f"Error adding shrinkwrap modifier: {str(e)}"
# ============================================================================
# WEIGHTED NORMAL MODIFIER
# ============================================================================
def add_weighted_normal_modifier(
ctx: Context,
blender_connection,
mode: str = "FACE_AREA",
weight: int = 50,
keep_sharp: bool = True,
face_influence: bool = True
) -> str:
"""
Add a Weighted Normal modifier to improve hard-surface shading.
Recalculates custom normals using face area or corner angle weighting
to improve shading on hard-surface models without adding geometry.
Parameters:
- mode: Weighting mode - 'FACE_AREA', 'CORNER_ANGLE', 'FACE_AREA_WITH_ANGLE' (default: 'FACE_AREA')
- weight: Weight influence 0-100 (default: 50)
- keep_sharp: Keep sharp edges (default: true)
- face_influence: Use face influence (default: true)
Returns status message with modifier details.
"""
try:
valid_modes = ['FACE_AREA', 'CORNER_ANGLE', 'FACE_AREA_WITH_ANGLE']
if mode not in valid_modes:
return f"Error: Invalid mode '{mode}'. Must be one of {valid_modes}."
if not 0 <= weight <= 100:
return f"Error: Weight must be between 0 and 100."
result = blender_connection.send_command("add_weighted_normal_modifier", {
"mode": mode,
"weight": weight,
"keep_sharp": keep_sharp,
"face_influence": face_influence
})
if "error" in result:
return f"Error: {result['error']}"
output = "Weighted Normal Modifier Added!\n\n"
output += f"Mode: {mode}\n"
output += f"Weight: {weight}\n"
output += f"Keep Sharp: {keep_sharp}\n"
output += f"Face Influence: {face_influence}\n"
return output
except Exception as e:
logger.error(f"Error adding weighted normal modifier: {str(e)}")
return f"Error adding weighted normal modifier: {str(e)}"
# ============================================================================
# DATA TRANSFER MODIFIER
# ============================================================================
def add_data_transfer_modifier(
ctx: Context,
blender_connection,
source: str,
data_types: List[str],
mapping: str = "NEAREST_FACE",
mix_factor: float = 1.0
) -> str:
"""
Add a Data Transfer modifier to copy data from high-poly to low-poly.
Transfers various data types (normals, UVs, vertex colors, etc.) from
a source object to the active object using specified mapping method.
Parameters:
- source: Name of the source object to transfer data from
- data_types: List of data types to transfer - ['CUSTOM_NORMAL', 'UV', 'VCOL', 'VGROUP_WEIGHTS']
- mapping: Mapping method - 'NEAREST_FACE', 'NEAREST_VERTEX', 'POLYINTERP_NEAREST', 'POLYINTERP_LNORPROJ' (default: 'NEAREST_FACE')
- mix_factor: Blend factor 0-1 (default: 1.0)
Returns status message with modifier details.
"""
try:
valid_data_types = ['CUSTOM_NORMAL', 'UV', 'VCOL', 'VGROUP_WEIGHTS']
for dt in data_types:
if dt not in valid_data_types:
return f"Error: Invalid data type '{dt}'. Must be one of {valid_data_types}."
valid_mappings = ['NEAREST_FACE', 'NEAREST_VERTEX', 'POLYINTERP_NEAREST', 'POLYINTERP_LNORPROJ']
if mapping not in valid_mappings:
return f"Error: Invalid mapping '{mapping}'. Must be one of {valid_mappings}."
if not 0.0 <= mix_factor <= 1.0:
return f"Error: Mix factor must be between 0.0 and 1.0."
result = blender_connection.send_command("add_data_transfer_modifier", {
"source": source,
"data_types": data_types,
"mapping": mapping,
"mix_factor": mix_factor
})
if "error" in result:
return f"Error: {result['error']}"
output = "Data Transfer Modifier Added!\n\n"
output += f"Source: {source}\n"
output += f"Data Types: {', '.join(data_types)}\n"
output += f"Mapping: {mapping}\n"
output += f"Mix Factor: {mix_factor}\n"
return output
except Exception as e:
logger.error(f"Error adding data transfer modifier: {str(e)}")
return f"Error adding data transfer modifier: {str(e)}"
# ============================================================================
# DECIMATE MODIFIER
# ============================================================================
def add_decimate_modifier(
ctx: Context,
blender_connection,
mode: str = "COLLAPSE",
ratio: float = 0.5,
iterations: int = 1
) -> str:
"""
Add a Decimate modifier to reduce polygon count predictably.
Reduces mesh complexity using collapse, dissolve, or un-subdivide modes
while attempting to preserve overall shape.
Parameters:
- mode: Decimation mode - 'COLLAPSE', 'DISSOLVE', 'UNSUBDIV' (default: 'COLLAPSE')
- ratio: For COLLAPSE: reduction ratio 0-1 (default: 0.5). For others: 1.0
- iterations: For UNSUBDIV: number of iterations (default: 1)
Returns status message with modifier details and predicted face count.
"""
try:
valid_modes = ['COLLAPSE', 'DISSOLVE', 'UNSUBDIV']
if mode not in valid_modes:
return f"Error: Invalid mode '{mode}'. Must be one of {valid_modes}."
if mode == 'COLLAPSE' and not 0.0 < ratio <= 1.0:
return f"Error: For COLLAPSE mode, ratio must be between 0 and 1."
if mode == 'UNSUBDIV' and iterations < 1:
return f"Error: For UNSUBDIV mode, iterations must be at least 1."
result = blender_connection.send_command("add_decimate_modifier", {
"mode": mode,
"ratio": ratio,
"iterations": iterations
})
if "error" in result:
return f"Error: {result['error']}"
output = "Decimate Modifier Added!\n\n"
output += f"Mode: {mode}\n"
if mode == 'COLLAPSE':
output += f"Ratio: {ratio}\n"
if mode == 'UNSUBDIV':
output += f"Iterations: {iterations}\n"
if 'face_count' in result:
output += f"Current Face Count: {result['face_count']}\n"
if 'predicted_faces' in result:
output += f"Predicted Face Count: {result['predicted_faces']}\n"
return output
except Exception as e:
logger.error(f"Error adding decimate modifier: {str(e)}")
return f"Error adding decimate modifier: {str(e)}"
# ============================================================================
# LAPLACIAN SMOOTH MODIFIER
# ============================================================================
def add_laplacian_smooth_modifier(
ctx: Context,
blender_connection,
factor: float = 1.0,
repeat: int = 2,
preserve_volume: bool = True
) -> str:
"""
Add a Laplacian Smooth modifier to reduce surface noise.
Applies Laplacian smoothing to reduce noise and high-frequency detail
while optionally preserving overall volume.
Parameters:
- factor: Smoothing factor 0-100 (default: 1.0)
- repeat: Number of iterations (default: 2)
- preserve_volume: Maintain mesh volume (default: true)
Returns status message with modifier details.
"""
try:
if not 0.0 <= factor <= 100.0:
return f"Error: Factor must be between 0 and 100."
if repeat < 1:
return f"Error: Repeat must be at least 1."
result = blender_connection.send_command("add_laplacian_smooth_modifier", {
"factor": factor,
"repeat": repeat,
"preserve_volume": preserve_volume
})
if "error" in result:
return f"Error: {result['error']}"
output = "Laplacian Smooth Modifier Added!\n\n"
output += f"Factor: {factor}\n"
output += f"Iterations: {repeat}\n"
output += f"Preserve Volume: {preserve_volume}\n"
return output
except Exception as e:
logger.error(f"Error adding laplacian smooth modifier: {str(e)}")
return f"Error adding laplacian smooth modifier: {str(e)}"