PolyHaven Material Swap
Applies multiple PolyHaven PBR textures to the same object one by one and renders each — perfect for showing product finish variants (matte black, glossy white, brushed metal, marble, etc.).
Adapted for claude-3d-harness. The original ran a bundled Node script that opened Blender's add-on socket itself and
sent it Python built from command-line text; it also switched Blender's Cycles device preferences for the whole
machine. The same steps now go through the MCP server, and the preferences are left alone. See
docs/security-review.md.
Parameters
| Parameter |
Description |
Default |
object |
Blender object name |
required |
textures |
PolyHaven texture IDs |
required (or use a preset) |
output |
Folder for the rendered images |
<JOB_DIR>/variants |
preset |
Texture set: metals, woods, stones, fabrics |
none |
resolution |
Texture resolution: 1k, 2k |
2k |
render_width / render_height |
Render size |
1920 x 1080 |
samples |
Render samples |
64 |
Presets
- metals — brushed_metal, rusty_metal, painted_metal_02, corrugated_iron
- woods — wood_cabinet_worn_long, plywood, bark_brown_02, wood_floor
- stones — marble_01, concrete_wall_003, rock_face, gravel_floor_02
- fabrics — fabric_pattern_07, leather_red_02, denim_fabric, burlap_01
Flow
Step 1: Remember what will change, then set up the render
import bpy
OBJECT = "<object name>"
scene = bpy.context.scene
r = scene.render
# So that step 4 can put everything back
scene["_swap_restore"] = {
"engine": r.engine, "samples": scene.cycles.samples, "denoise": scene.cycles.use_denoising,
"res_x": r.resolution_x, "res_y": r.resolution_y, "format": r.image_settings.file_format,
"filepath": r.filepath,
}
obj = bpy.data.objects.get(OBJECT)
if obj and obj.data.materials and obj.data.materials[0]:
scene["_original_mat"] = obj.data.materials[0].name
r.engine = 'CYCLES'
scene.cycles.samples = 64
scene.cycles.use_denoising = True
r.resolution_x, r.resolution_y = 1920, 1080
r.image_settings.file_format = 'PNG'
# A camera only if the scene has none
if scene.camera is None:
import mathutils
bpy.ops.object.camera_add(location=(2.5, -2.5, 1.5))
cam = bpy.context.active_object
cam.name = 'Variants_Camera'
scene.camera = cam
direction = mathutils.Vector((0, 0, 0)) - cam.location
cam.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
Step 2: For each texture — download, apply, render
download_polyhaven_asset(asset_id=<texture>, asset_type="textures", resolution=<resolution>, file_format="jpg")
returns the material name. If a download fails, skip that texture and say so.
import bpy
OBJECT = "<object name>"
MATERIAL = "<material name the download returned>"
OUT = "<JOB_DIR>/variants/<texture id>.png"
obj = bpy.data.objects.get(OBJECT)
mat = bpy.data.materials.get(MATERIAL)
if obj and mat:
if obj.data.materials:
obj.data.materials[0] = mat
else:
obj.data.materials.append(mat)
bpy.context.scene.render.filepath = OUT
bpy.ops.render.render(write_still=True)
Step 3: Look at every render
Read each image before reporting: a variant that rendered black or untextured is a failed variant.
Step 4: Put the scene back
import bpy
OBJECT = "<object name>"
scene = bpy.context.scene
obj = bpy.data.objects.get(OBJECT)
orig_name = scene.get("_original_mat")
if obj and orig_name:
mat = bpy.data.materials.get(orig_name)
if mat and obj.data.materials:
obj.data.materials[0] = mat
saved = scene.get("_swap_restore")
if saved:
r = scene.render
r.engine = saved["engine"]
scene.cycles.samples = saved["samples"]
scene.cycles.use_denoising = saved["denoise"]
r.resolution_x, r.resolution_y = saved["res_x"], saved["res_y"]
r.image_settings.file_format = saved["format"]
r.filepath = saved["filepath"]
for key in ("_swap_restore", "_original_mat"):
if key in scene:
del scene[key]
1---2name: polyhaven-material-swap3description: Cycle through multiple PolyHaven materials on a product and render each variant. Great for showing color/finish options. Trigger when asked to show material variants, swap textures, create color options, show finish comparisons, or render product in different materials.4---56# PolyHaven Material Swap78Applies multiple PolyHaven PBR textures to the same object one by one and renders each — perfect for showing product finish variants (matte black, glossy white, brushed metal, marble, etc.).910> Adapted for claude-3d-harness. The original ran a bundled Node script that opened Blender's add-on socket itself and11> sent it Python built from command-line text; it also switched Blender's Cycles device preferences for the whole12> machine. The same steps now go through the MCP server, and the preferences are left alone. See13> `docs/security-review.md`.1415## Parameters1617| Parameter | Description | Default |18|-----------|-------------|---------|19| `object` | Blender object name | **required** |20| `textures` | PolyHaven texture IDs | **required** (or use a preset) |21| `output` | Folder for the rendered images | `<JOB_DIR>/variants` |22| `preset` | Texture set: metals, woods, stones, fabrics | none |23| `resolution` | Texture resolution: 1k, 2k | `2k` |24| `render_width` / `render_height` | Render size | `1920` x `1080` |25| `samples` | Render samples | `64` |2627## Presets2829- **metals** — brushed_metal, rusty_metal, painted_metal_02, corrugated_iron30- **woods** — wood_cabinet_worn_long, plywood, bark_brown_02, wood_floor31- **stones** — marble_01, concrete_wall_003, rock_face, gravel_floor_0232- **fabrics** — fabric_pattern_07, leather_red_02, denim_fabric, burlap_013334## Flow3536### Step 1: Remember what will change, then set up the render3738```python39import bpy4041OBJECT = "<object name>"42scene = bpy.context.scene43r = scene.render4445# So that step 4 can put everything back46scene["_swap_restore"] = {47 "engine": r.engine, "samples": scene.cycles.samples, "denoise": scene.cycles.use_denoising,48 "res_x": r.resolution_x, "res_y": r.resolution_y, "format": r.image_settings.file_format,49 "filepath": r.filepath,50}51obj = bpy.data.objects.get(OBJECT)52if obj and obj.data.materials and obj.data.materials[0]:53 scene["_original_mat"] = obj.data.materials[0].name5455r.engine = 'CYCLES'56scene.cycles.samples = 6457scene.cycles.use_denoising = True58r.resolution_x, r.resolution_y = 1920, 108059r.image_settings.file_format = 'PNG'6061# A camera only if the scene has none62if scene.camera is None:63 import mathutils64 bpy.ops.object.camera_add(location=(2.5, -2.5, 1.5))65 cam = bpy.context.active_object66 cam.name = 'Variants_Camera'67 scene.camera = cam68 direction = mathutils.Vector((0, 0, 0)) - cam.location69 cam.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()70```7172### Step 2: For each texture — download, apply, render7374`download_polyhaven_asset(asset_id=<texture>, asset_type="textures", resolution=<resolution>, file_format="jpg")`75returns the material name. If a download fails, skip that texture and say so.7677```python78import bpy7980OBJECT = "<object name>"81MATERIAL = "<material name the download returned>"82OUT = "<JOB_DIR>/variants/<texture id>.png"8384obj = bpy.data.objects.get(OBJECT)85mat = bpy.data.materials.get(MATERIAL)86if obj and mat:87 if obj.data.materials:88 obj.data.materials[0] = mat89 else:90 obj.data.materials.append(mat)91 bpy.context.scene.render.filepath = OUT92 bpy.ops.render.render(write_still=True)93```9495### Step 3: Look at every render9697Read each image before reporting: a variant that rendered black or untextured is a failed variant.9899### Step 4: Put the scene back100101```python102import bpy103104OBJECT = "<object name>"105scene = bpy.context.scene106obj = bpy.data.objects.get(OBJECT)107108orig_name = scene.get("_original_mat")109if obj and orig_name:110 mat = bpy.data.materials.get(orig_name)111 if mat and obj.data.materials:112 obj.data.materials[0] = mat113114saved = scene.get("_swap_restore")115if saved:116 r = scene.render117 r.engine = saved["engine"]118 scene.cycles.samples = saved["samples"]119 scene.cycles.use_denoising = saved["denoise"]120 r.resolution_x, r.resolution_y = saved["res_x"], saved["res_y"]121 r.image_settings.file_format = saved["format"]122 r.filepath = saved["filepath"]123for key in ("_swap_restore", "_original_mat"):124 if key in scene:125 del scene[key]126```