MCP backend
Use when: Any lighting setup or modification in Blender. Includes HDRI/environment lighting and individual lamp placement.
Before the first Blender call, read ../text-to-blender/references/mcp-backends.md. Prefer official MCP for shared capabilities; preserve the selected backend for the whole task. Tool names in recipes are client/legacy examples: map them through that reference and its agent-host guide.
Blender Lighting
Light scenes the way pros do: with structure, intent, and physically reasonable values.
The five light types
| Type | Behavior | Use for |
|---|---|---|
| AREA | Light from a rectangular surface; soft shadows automatic | 80% of cases. Window, softbox, fluorescent panel |
| SUN | Parallel rays from "infinity" | Sunlight, moonlight, distant directional |
| POINT | Omnidirectional from a point | Bulbs, candles, small omnis |
| SPOT | Cone with falloff | Stage lights, headlights, focused beams |
| HDRI/World | 360° environment image | Realistic ambient, outdoor, product photography |
Default rule: Use Area lights for almost everything except the sun. Soft shadows come for free.
Decision tree
What's the mood? ├── Studio / commercial → Three-point lighting (key+fill+rim) + HDRI fill 0.3 ├── Outdoor / sunlit → Sun + HDRI sky environment ├── Indoor cinematic → Sun through window + HDRI low + practicals (lamps as Point) ├── Dramatic / noir → Single Spot at high angle, no fill ├── Stylized / cartoon → Three-point with high contrast + saturated key color └── Unsure → Three-point with HDRI grounding (works for 90% of cases)
Reference-look handoff
If the goal is to match an original/reference image rather than make a generally attractive render, chain-load reference-look-calibration. It owns measurement of hue/saturation/value, object extent, glow/aura color, and before/after look metrics. This skill should then apply the requested material/lighting/render changes within that calibrated target.
Recipes
Helper: aim_at(light, target) — required for subject-aware lighting
Recipe 1 below positions lights at fixed world coords with hardcoded rotations. That's fine for a generic 1m subject at the world origin. For ANY other subject (small jewellery, tall sword, sprawling building), you need lights aimed at the subject. Use this helper:
pythonfrom mathutils import Vector def aim_at(light_obj, target): """Aim a light at a world-space target. target may be a Vector or a tuple/list (x, y, z) or a Blender object. """ target_pos = Vector(target.location) if hasattr(target, 'location') else Vector(target) direction = (target_pos - light_obj.location).normalized() light_obj.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
Helper: scene-aware light positioning
pythonfrom mathutils import Vector def compute_scene_bbox_center(meshes): """Average bbox center over a list of mesh objects (world space).""" import bpy deps = bpy.context.evaluated_depsgraph_get() all_verts = [] for o in meshes: eval_obj = o.evaluated_get(deps) em = eval_obj.to_mesh() for v in em.vertices: all_verts.append(o.matrix_world @ v.co) eval_obj.to_mesh_clear() xs = [v.x for v in all_verts] ys = [v.y for v in all_verts] zs = [v.z for v in all_verts] center = Vector(((min(xs)+max(xs))/2, (min(ys)+max(ys))/2, (min(zs)+max(zs))/2)) extent = max(max(xs)-min(xs), max(ys)-min(ys), max(zs)-min(zs)) return center, extent
Recipe 0a — Subject-CLASS-aware three-point lighting (use this for orchestrator E2E)
Generic three-point lighting (Recipe 0b below) places lights at fixed energy ratios. That works for opaque subjects (chair, sword) but breaks for glass (rim washes out volume tint) and is too cool for wood (loses warmth).
Pass a subject_class hint to tune the setup:
| Class | Key:Fill:Rim ratio | Key color temp | Reason |
|---|---|---|---|
'metal' | 4:1:2 (default) | warm 3200K | Standard 3-point reads metallic well |
'glass' | 3:1:1.2 | neutral 5500K | Soft rim — strong rim WASHES OUT volume tint; brighter fill so transmission shows colour |
'wood' | 4:1:1.5 | warm 3000K | Warmer key brings out wood tones; less rim (wood doesn't need silhouette boost) |
'fabric' | 3:1:0.5 | neutral 5500K | Soft and balanced; sheen reads in fill light |
'skin' | 4:1:1 | warm 3500K | Warm key for healthy tone; subtle rim (avoids harsh edges on faces) |
'product' | 5:1:1.5 | neutral 5000K | Higher contrast; commercial/clean look |
| (unspecified) | falls back to Recipe 0b (default) | warm 3200K |
pythonimport bpy, math from mathutils import Vector def aim_at(light_obj, target): target_pos = Vector(target.location) if hasattr(target, 'location') else Vector(target) direction = (target_pos - light_obj.location).normalized() light_obj.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler() def apply_three_point(subject_class='metal'): """Configure 3-point lighting with subject-class-aware ratios. subject_class: 'metal' | 'glass' | 'wood' | 'fabric' | 'skin' | 'product' | 'metal' (default) """ profiles = { 'metal': dict(ratio=(4.0, 1.0, 2.0), key_color=(1.0, 0.95, 0.85), fill_color=(0.85, 0.9, 1.0), rim_color=(0.7, 0.85, 1.0)), 'glass': dict(ratio=(3.0, 1.0, 1.2), key_color=(1.0, 0.98, 0.95), fill_color=(0.95, 0.95, 1.0), rim_color=(0.95, 0.95, 1.0)), 'wood': dict(ratio=(4.0, 1.0, 1.5), key_color=(1.0, 0.92, 0.78), fill_color=(0.95, 0.95, 1.0), rim_color=(0.85, 0.92, 1.0)), 'fabric': dict(ratio=(3.0, 1.0, 0.5), key_color=(1.0, 0.97, 0.92), fill_color=(0.92, 0.95, 1.0), rim_color=(0.95, 0.95, 1.0)), 'skin': dict(ratio=(4.0, 1.0, 1.0), key_color=(1.0, 0.93, 0.82), fill_color=(0.95, 0.96, 1.0), rim_color=(0.92, 0.92, 1.0)), 'product': dict(ratio=(5.0, 1.0, 1.5), key_color=(1.0, 0.98, 0.95), fill_color=(0.98, 0.98, 1.0), rim_color=(0.98, 0.98, 1.0)), } p = profiles.get(subject_class, profiles['metal']) # Compute scene bbox subject_meshes = [o for o in bpy.data.objects if o.type == 'MESH' and o.name.startswith('GEO-')] deps = bpy.context.evaluated_depsgraph_get() all_verts = [] for o in subject_meshes: eo = o.evaluated_get(deps); em = eo.to_mesh() for v in em.vertices: all_verts.append(o.matrix_world @ v.co) eo.to_mesh_clear() xs = [v.x for v in all_verts]; ys = [v.y for v in all_verts]; zs = [v.z for v in all_verts] center = Vector(((min(xs)+max(xs))/2, (min(ys)+max(ys))/2, (min(zs)+max(zs))/2)) biggest = max(max(zs)-min(zs), max(xs)-min(xs)) light_dist = max(biggest * 1.5, 1.0) # Energy scales with distance² base_energy = 100 * (light_dist / 1.5) ** 2 key_e, fill_e, rim_e = (base_energy * r for r in p['ratio']) # Remove existing lights for o in list(bpy.data.objects): if o.type == 'LIGHT' and (o.name.startswith('LGT-key') or o.name.startswith('LGT-fill') or o.name.startswith('LGT-rim')): bpy.data.objects.remove(o, do_unlink=True) # KEY (warm, front-right above) key = bpy.data.objects.new('LGT-key', bpy.data.lights.new('LGT-key', type='AREA')) key.data.energy = key_e; key.data.size = 0.5; key.data.color = p['key_color'] bpy.context.collection.objects.link(key) key.location = (center.x + light_dist*0.7, center.y - light_dist*0.7, center.z + light_dist*0.5) aim_at(key, center) # FILL (cool, opposite, weaker) fill = bpy.data.objects.new('LGT-fill', bpy.data.lights.new('LGT-fill', type='AREA')) fill.data.energy = fill_e; fill.data.size = 1.0; fill.data.color = p['fill_color'] bpy.context.collection.objects.link(fill) fill.location = (center.x - light_dist*0.7, center.y - light_dist*0.5, center.z + light_dist*0.3) aim_at(fill, center) # RIM rim_type = 'AREA' if subject_class == 'glass' else 'SPOT' rim = bpy.data.objects.new('LGT-rim', bpy.data.lights.new('LGT-rim', type=rim_type)) rim.data.energy = rim_e; rim.data.color = p['rim_color'] if rim_type == 'AREA': rim.data.size = 1.5 # larger soft-source for glass else: rim.data.spot_size = math.radians(50) bpy.context.collection.objects.link(rim) rim.location = (center.x, center.y + light_dist, center.z + light_dist*0.5) aim_at(rim, center) print(f"lighting:{subject_class} key:fill:rim={p['ratio']} dist={light_dist:.2f}m") # Usage: # apply_three_point('glass') # for the wine bottle # apply_three_point('wood') # for the chair # apply_three_point('metal') # for the sword (or omit; 'metal' is default)
Recipe 0b — Three-point lighting aimed at a subject (generic, no class hint)
Use this instead of Recipe 1 when you have a specific subject but the class doesn't matter. Lights are placed proportionally to the subject's largest dimension.
pythonimport bpy, math from mathutils import Vector def aim_at(light_obj, target): target_pos = Vector(target.location) if hasattr(target, 'location') else Vector(target) direction = (target_pos - light_obj.location).normalized() light_obj.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler() # Determine subject and its scale subject_meshes = [o for o in bpy.data.objects if o.type == 'MESH' and o.name.startswith('GEO-')] deps = bpy.context.evaluated_depsgraph_get() all_verts = [] for o in subject_meshes: eo = o.evaluated_get(deps); em = eo.to_mesh() for v in em.vertices: all_verts.append(o.matrix_world @ v.co) eo.to_mesh_clear() xs = [v.x for v in all_verts]; ys = [v.y for v in all_verts]; zs = [v.z for v in all_verts] center = Vector(((min(xs)+max(xs))/2, (min(ys)+max(ys))/2, (min(zs)+max(zs))/2)) extent = max(max(xs)-min(xs), max(ys)-min(ys), max(zs)-min(zs)) light_dist = max(extent * 1.5, 1.0) # Energy values scale roughly inversely with squared distance from subject — recipe targets # physically reasonable values for a ~1m subject at ~1.5m light distance. key_energy = 100 * (light_dist / 1.5) ** 2 fill_energy = key_energy * 0.3 rim_energy = key_energy * 0.8 # KEY (warm, front-right above) key = bpy.data.objects.new('LGT-key', bpy.data.lights.new('LGT-key', type='AREA')) key.data.energy = key_energy; key.data.size = 0.5; key.data.color = (1.0, 0.95, 0.85) bpy.context.collection.objects.link(key) key.location = (center.x + light_dist * 0.7, center.y - light_dist * 0.7, center.z + light_dist * 0.5) aim_at(key, center) # FILL (cool, opposite, weaker) fill = bpy.data.objects.new('LGT-fill', bpy.data.lights.new('LGT-fill', type='AREA')) fill.data.energy = fill_energy; fill.data.size = 1.0; fill.data.color = (0.85, 0.9, 1.0) bpy.context.collection.objects.link(fill) fill.location = (center.x - light_dist * 0.7, center.y - light_dist * 0.5, center.z + light_dist * 0.3) aim_at(fill, center) # RIM (cool, behind, separates subject from BG) rim = bpy.data.objects.new('LGT-rim', bpy.data.lights.new('LGT-rim', type='SPOT')) rim.data.energy = rim_energy; rim.data.color = (0.7, 0.85, 1.0); rim.data.spot_size = math.radians(50) bpy.context.collection.objects.link(rim) rim.location = (center.x, center.y + light_dist, center.z + light_dist * 0.5) aim_at(rim, center) print(f'lighting:three_point_aimed center={tuple(round(v,2) for v in center)} extent={extent:.2f}m dist={light_dist:.2f}m')
Recipe 0c — Practical lighting (scene contains its own emissive light source)
When the subject IS or CONTAINS a light source — desk lamp with bulb, candle with flame, monitor with glowing screen, neon sign — the scene needs a different setup:
- Make the world background dark (Strength 0.10–0.20). Otherwise the bulb's contribution is drowned out by ambient.
- Reduce or remove the standard 3-point fill/rim. The practical light should dominate.
- Keep a subtle ambient fill (8-15W Area light from camera direction) so the lamp body itself is visible — pure practical-only renders make the lamp shape silhouette into shadow.
- Tune emission strength HIGH for small mesh emitters (see
blender-materialsRecipe 11b — bulb spheres need Strength 800-3000 to read like real bulbs). - Cycles
max_bounces≥ 16 for proper interior-shade lighting — the bulb's light needs to bounce inside the shade and out through the opening.
pythonimport bpy from mathutils import Vector def aim_at(light_obj, target): target_pos = Vector(target.location) if hasattr(target, 'location') else Vector(target) direction = (target_pos - light_obj.location).normalized() light_obj.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler() # Dim world (let the practical dominate) world = bpy.context.scene.world world.use_nodes = True nodes = world.node_tree.nodes for n in list(nodes): nodes.remove(n) output = nodes.new('ShaderNodeOutputWorld') bg = nodes.new('ShaderNodeBackground') bg.inputs['Color'].default_value = (0.02, 0.02, 0.03, 1.0) bg.inputs['Strength'].default_value = 0.15 world.node_tree.links.new(bg.outputs['Background'], output.inputs['Surface']) # Single subtle ambient fill from camera direction fill = bpy.data.objects.new('LGT-ambient_fill', bpy.data.lights.new('LGT-ambient_fill', type='AREA')) fill.data.energy = 8; fill.data.size = 1.0; fill.data.color = (0.85, 0.9, 1.0) bpy.context.collection.objects.link(fill) fill.location = (0.5, -0.8, 0.5) aim_at(fill, Vector((0, 0, 0.3))) # Cycles bounces scene = bpy.context.scene scene.cycles.max_bounces = 16 print('lighting:practical_setup')
The practical light's emission shader (mesh-emissive bulb / candle flame / etc.) handles the rest. Scene appears like real photography of an illuminated subject — dark surroundings, warm pool of light from the practical, subject silhouette gently filled.
Validation proof: see text-to-blender/assets/v1.1.0-validation/desk_lamp_emission.webp for what this setup produces (desk lamp with visible bulb glow, warm light pool on desk surface, lamp body visible against the dark scene).
Recipe 1 — Three-point lighting (the canonical setup)
pythonimport bpy, math # KEY LIGHT (warm, front-right) key_data = bpy.data.lights.new('LGT-key', type='AREA') key_data.energy = 1000 key_data.size = 1.0 key_data.color = (1.0, 0.95, 0.85) # warm tungsten ~3200K key = bpy.data.objects.new('LGT-key', key_data) bpy.context.collection.objects.link(key) key.location = (3, -3, 3.5) key.rotation_euler = (math.radians(35), math.radians(45), 0) # FILL LIGHT (cool, front-left, weaker) fill_data = bpy.data.lights.new('LGT-fill', type='AREA') fill_data.energy = 300 fill_data.size = 2.0 fill_data.color = (0.85, 0.9, 1.0) # cool sky ~6500K fill = bpy.data.objects.new('LGT-fill', fill_data) bpy.context.collection.objects.link(fill) fill.location = (-3, -2, 2.5) fill.rotation_euler = (math.radians(50), math.radians(-45), 0) # BACK / RIM LIGHT (cool, behind subject) rim_data = bpy.data.lights.new('LGT-rim', type='SPOT') rim_data.energy = 600 rim_data.spot_size = math.radians(40) rim_data.color = (0.7, 0.85, 1.0) rim = bpy.data.objects.new('LGT-rim', rim_data) bpy.context.collection.objects.link(rim) rim.location = (0, 4, 3.0) rim.rotation_euler = (math.radians(120), 0, math.radians(180)) print('lighting:three_point')
Standard ratios:
- High-key (commercial): Key:Fill = 2:1
- Medium (portrait): 4:1
- Low-key (dramatic): 8:1+
- Rim: 50–100% of key
Recipe 2 — HDRI environment
pythonimport bpy world = bpy.context.scene.world world.use_nodes = True nodes = world.node_tree.nodes links = world.node_tree.links # Wipe existing world nodes for n in list(nodes): nodes.remove(n) # Output → Background ← Environment Texture ← Mapping ← Texture Coordinate output = nodes.new('ShaderNodeOutputWorld'); output.location = (300, 0) bg = nodes.new('ShaderNodeBackground'); bg.location = (100, 0) bg.inputs['Strength'].default_value = 1.0 env = nodes.new('ShaderNodeTexEnvironment'); env.location = (-100, 0) env.image = bpy.data.images.load('/path/to/your.hdr') # ← user provides path mapping = nodes.new('ShaderNodeMapping'); mapping.location = (-300, 0) tex_coord = nodes.new('ShaderNodeTexCoord'); tex_coord.location = (-500, 0) links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector']) links.new(mapping.outputs['Vector'], env.inputs['Vector']) links.new(env.outputs['Color'], bg.inputs['Color']) links.new(bg.outputs['Background'], output.inputs['Surface']) print('lighting:hdri')
Pro source: free HDRIs at polyhaven.com/hdris (CC0).
Recipe 3 — Sunny outdoor
pythonimport bpy, math sun_data = bpy.data.lights.new('LGT-sun', type='SUN') sun_data.energy = 5.0 sun_data.color = (1.0, 0.95, 0.8) # golden hour sun_data.angle = math.radians(0.5) # realistic sun size; bigger = softer sun = bpy.data.objects.new('LGT-sun', sun_data) bpy.context.collection.objects.link(sun) sun.location = (0, 0, 10) sun.rotation_euler = (math.radians(45), math.radians(15), 0) print('lighting:outdoor_sun')
Combine with HDRI sky environment (Recipe 2) for natural ambient fill.
Recipe 4 — Indoor window light
pythonimport bpy, math # Sun coming through window — cool blue, sharp sun_data = bpy.data.lights.new('LGT-window_sun', type='SUN') sun_data.energy = 3.0 sun_data.color = (0.85, 0.9, 1.0) sun_data.angle = math.radians(2.0) # softer than direct sun sun = bpy.data.objects.new('LGT-window_sun', sun_data) bpy.context.collection.objects.link(sun) sun.location = (5, -3, 4) sun.rotation_euler = (math.radians(60), math.radians(-30), 0) # Practical lamp — warm point light lamp_data = bpy.data.lights.new('LGT-lamp', type='POINT') lamp_data.energy = 60 lamp_data.color = (1.0, 0.7, 0.4) # warm bulb lamp = bpy.data.objects.new('LGT-lamp', lamp_data) bpy.context.collection.objects.link(lamp) lamp.location = (-1, 2, 1.5) print('lighting:indoor_window')
Recipe 5 — Dramatic single-source
pythonimport bpy, math spot_data = bpy.data.lights.new('LGT-drama', type='SPOT') spot_data.energy = 800 spot_data.spot_size = math.radians(30) spot_data.spot_blend = 0.3 spot_data.color = (1.0, 0.95, 0.85) spot = bpy.data.objects.new('LGT-drama', spot_data) bpy.context.collection.objects.link(spot) spot.location = (2, -2, 6) spot.rotation_euler = (math.radians(60), 0, 0) print('lighting:dramatic')
For full noir: pair with strong volumetrics (atmosphere) — see references/overview.md for the volumetric setup.
Recipe 6 — Color-temperature cheat sheet
| Source | RGB |
|---|---|
| Candle (1850K) | (1.0, 0.6, 0.3) |
| Tungsten (3200K) | (1.0, 0.85, 0.6) |
| LED warm (3000K) | (1.0, 0.8, 0.6) |
| Sunset / golden (3500K) | (1.0, 0.85, 0.65) |
| Daylight noon (5500K) | (1.0, 1.0, 1.0) |
| Overcast sky (6500K) | (0.95, 0.95, 1.0) |
| Blue hour (8000K) | (0.8, 0.9, 1.0) |
Pro mix: Warm key (tungsten) + cool fill (daylight) = the "golden/teal" Hollywood look.
Recipe 7 — Soft vs hard shadow tweak
pythonimport bpy, math light_data = bpy.data.lights['LGT-key'] # Make shadows softer light_data.size = 2.0 # Area: bigger size = softer shadow # Or for Sun: # light_data.angle = math.radians(5) # bigger angle = softer shadow # Make shadows harder (crisp) # light_data.size = 0.1 # Or: # light_data.angle = math.radians(0.5)
Naming convention
| Prefix | Meaning |
|---|---|
LGT-key | Main / key light |
LGT-fill | Fill light |
LGT-rim / LGT-back | Rim or back light |
LGT-sun | Sun lamp |
LGT-{name} | Practical lights (lamp, candle, neon, etc.) |
Common pitfalls
| Symptom | Fix |
|---|---|
| Half the model in pitch black | Add fill (Area light or HDRI) |
| Render looks "flat" | Increase key:fill ratio; add rim |
| Hard shadows everywhere | Increase Area size or Sun angle |
| Too dark overall | Boost View Transform exposure or HDRI strength |
| No reflections on materials | Always set a world environment (HDRI) |
| Light inside object | Check world position; light must be visible from camera |
| Backlight blowing out subject | Rim energy ≤ key energy |
When to load references/overview.md
Load when:
- The user asks for a setup not in the recipes (volumetric god rays, light groups, light linking)
- HDRI rotation / strength tuning is needed beyond defaults
- Multi-light scenes (5+ lamps) require organization
- Color science or color management gets specific (AgX, Filmic)
The reference covers: all 5 light types in depth, full HDRI workflow, light groups for re-lighting in compositor, recipes for product/portrait/architectural/character/animation looks.


