references/overview.md
# Lighting — Pro Knowledge Overview
**Domain**: 07 — Three-point, HDRI, studio, dramatic, environmental
**Status**: Initial pass complete
**Last update**: 2026-04-27
---
## The light types Blender gives you
| Type | What it is | When to use |
|------|-----------|-------------|
| **Point** | Light from a single point in all directions | Bulbs, candles, small omnidirectional sources |
| **Sun** | Parallel rays (infinitely far) | Sunlight, moonlight, distant strong sources |
| **Spot** | Cone of light (with falloff) | Stage lights, headlights, focused beams |
| **Area** | Light from a rectangular/disk surface | Soft fill light, window light, softboxes |
| **HDRI / World** | 360° environment image | Realistic ambient, outdoor scenes, product photography |
**Default rule**: For physically-correct lighting, use Area lights for everything except sun. Area lights produce soft shadows automatically (no fiddling with shadow blur).
---
## Three-point lighting (the canonical setup)
```
[KEY LIGHT]
brightest, defines mood
typically 45° front-right
\
\
v
[FILL LIGHT] → [ SUBJECT ] ← [BACK / RIM LIGHT]
shadow softener, edge separation,
opposite key light, behind subject,
1/4 to 1/2 brightness cool color,
of key 1/2 to equal key
```
**Recipe in Python**:
```python
import bpy
import math
subject_loc = (0, 0, 1.6) # eye level
# KEY LIGHT (front-right, 45° elevation, warm)
key_data = bpy.data.lights.new('LGT-key', type='AREA')
key_data.energy = 1000 # watts
key_data.size = 1.0 # softbox size
key_data.color = (1.0, 0.95, 0.85) # warm tungsten
key_obj = bpy.data.objects.new('LGT-key', key_data)
bpy.context.collection.objects.link(key_obj)
key_obj.location = (3, -3, 3.5)
key_obj.rotation_euler = (math.radians(35), math.radians(45), 0)
# FILL LIGHT (front-left, lower power, cool)
fill_data = bpy.data.lights.new('LGT-fill', type='AREA')
fill_data.energy = 300 # 1/3 of key
fill_data.size = 2.0 # bigger = softer
fill_data.color = (0.85, 0.9, 1.0) # cool sky-tone
fill_obj = bpy.data.objects.new('LGT-fill', fill_data)
bpy.context.collection.objects.link(fill_obj)
fill_obj.location = (-3, -2, 2.5)
fill_obj.rotation_euler = (math.radians(50), math.radians(-45), 0)
# BACK/RIM LIGHT (behind subject, slightly above)
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) # cooler blue rim
rim_obj = bpy.data.objects.new('LGT-rim', rim_data)
bpy.context.collection.objects.link(rim_obj)
rim_obj.location = (0, 4, 3.0)
rim_obj.rotation_euler = (math.radians(120), 0, math.radians(180))
```
**Standard ratios**:
- **High-key (commercial, fashion)** — Key:Fill = 2:1 (low contrast, flat)
- **Medium (portrait)** — Key:Fill = 4:1 (balanced)
- **Low-key (dramatic, noir)** — Key:Fill = 8:1 or higher (heavy shadows)
- **Rim** — typically 50–100% of key (just enough to separate from background)
---
## HDRI lighting (the realism trick)
An **HDRI** (High Dynamic Range Image) is a 360° photograph stored with extended brightness range. Lighting from one HDRI gives realistic ambient, reflections, and color cast — way more convincing than lights alone.
### Setup HDRI as world background
```python
import bpy
world = bpy.context.scene.world
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
# Clear default
for node in list(nodes):
nodes.remove(node)
# Output node
output = nodes.new('ShaderNodeOutputWorld')
output.location = (300, 0)
# Background shader
bg = nodes.new('ShaderNodeBackground')
bg.location = (100, 0)
bg.inputs['Strength'].default_value = 1.0
# Environment Texture (HDRI)
env = nodes.new('ShaderNodeTexEnvironment')
env.location = (-100, 0)
env.image = bpy.data.images.load('/path/to/hdri.hdr')
# Mapping (rotate HDRI)
mapping = nodes.new('ShaderNodeMapping')
mapping.location = (-300, 0)
mapping.inputs['Rotation'].default_value = (0, 0, 1.5708) # 90° Z
# Texture Coordinate (Generated for environment)
tex_coord = nodes.new('ShaderNodeTexCoord')
tex_coord.location = (-500, 0)
# Wire
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'])
```
**Pro source**: [Poly Haven](https://polyhaven.com/hdris) — free, CC0, professional-quality HDRIs. The category "Studio" includes pre-lit photo studio environments.
### Combine HDRI + lights
HDRI alone is often too even/flat. Standard pro combo:
- **HDRI as fill** at strength 0.3–0.5 (subtle ambient)
- **One or two key lights** for direction/shape
This is "HDRI-grounded three-point lighting" — best of both.
---
## Lighting recipes by scenario
### Studio product shot (hero render)
- HDRI: Studio HDRI from Poly Haven, strength 0.5
- Key: Area light, 1m × 1m, energy 1000W, warm
- Fill: Area light, 2m × 2m, energy 300W, neutral
- Back: Spot, energy 500W, cool blue
- World tone: dark grey (Color Management → look: Medium High Contrast)
### Sunlit outdoor
- Sun light: Strength 5, color (1.0, 0.95, 0.8) for golden hour
- HDRI: Outdoor sky HDRI at strength 1.0
- No fill light needed (HDRI provides ambient)
- Optional bounce card (white plane, low emission) for fill
### Cinematic interior (window light)
- Sun light through window: Strength 3, color cool blue (0.85, 0.9, 1.0)
- HDRI: Indoor low-light at strength 0.2
- Practical lights (lamps, candles): Point lights with warm color (1.0, 0.7, 0.4)
- World volumetrics: subtle haze (Volume Scatter)
### Dramatic / film noir
- Single Spot light, harsh angle (60° from above)
- No fill light (or very minimal, 1/8 of key)
- Cool ambient via HDRI at strength 0.1
- Fog/haze via Volume Scatter for light shafts
### Animation studio look (cartoon / stylized)
- Three-point with high contrast (Key:Fill 4:1+)
- Use Suns with hard shadows (size = 0)
- Saturated key (slightly oversaturate base color)
- Toon Shader instead of Principled BSDF for materials
---
## Shadow control
### Soft vs hard shadows
- **Soft shadows** = Area light with size > 0.5m, OR Sun with size > 0.05
- **Hard shadows** = Point light or Sun with size = 0
- **Pro rule**: hardly anything in nature has truly hard shadows. Soft shadows always look more realistic.
### Adjusting shadow blur
```python
# For Area lights, the size IS the softness
light_data.size = 1.0 # 1m × 1m area
light_data.size_y = 0.5 # for rectangular area lights
# For Sun, "angle" controls shadow softness
sun_data.angle = math.radians(0.5) # 0.5° = realistic sun
sun_data.angle = math.radians(5) # blurry shadows (overcast)
```
---
## Color temperature reference
| Source | Temp (K) | RGB rough |
|--------|----------|-----------|
| Candle | 1850 | (1.0, 0.6, 0.3) |
| Tungsten bulb | 3200 | (1.0, 0.85, 0.6) |
| Halogen | 3400 | (1.0, 0.9, 0.7) |
| LED warm | 3000 | (1.0, 0.8, 0.6) |
| Sunset/golden hour | 3500 | (1.0, 0.85, 0.65) |
| Daylight (noon) | 5500 | (1.0, 1.0, 1.0) |
| Overcast sky | 6500 | (0.95, 0.95, 1.0) |
| Blue hour | 8000 | (0.8, 0.9, 1.0) |
| Shade/sky | 10000 | (0.7, 0.85, 1.0) |
**Pro mix**: Key warm (3200K), fill cool (5500K) — creates the "golden/teal" Hollywood look.
---
## Common pitfalls
| Mistake | Why | Fix |
|---------|-----|-----|
| Single Sun light, no fill | Half the model is in pitch black | Add fill via Area light or HDRI |
| All lights at 1000W | Looks flat, no contrast | Use ratios: Key:Fill 4:1, Key:Rim 1:1 |
| Hard shadows only | Looks artificial | Use Area lights (Cycles) or large sun angle (EEVEE/Cycles) |
| Forgot HDRI = no reflections | Materials look matte/dull | Always set a world environment, even for indoor scenes |
| Lights inside objects | "Why is everything dark?" | Check that lights are outside geometry; visible from camera |
| Backlight too strong | Subject silhouettes lose detail | Rim should be ≤ 1× key brightness |
| Overusing volumetrics | Hazy mess; render time explodes | Volumetric Strength 0.05–0.1 max |
---
## Light Groups (Cycles 3.6+)
Render multiple lighting setups in one render via "Light Groups" — assign each light to a group, then in the compositor mix them with different intensities.
```python
import bpy
# Assign light to a Light Group
light_obj.lightgroup = 'LG-key' # named group
# In View Layer settings:
view_layer = bpy.context.view_layer
view_layer.use_pass_combined = True
view_layer.lightgroups.add().name = 'LG-key'
view_layer.lightgroups.add().name = 'LG-fill'
view_layer.lightgroups.add().name = 'LG-rim'
```
In the compositor, each light group becomes a render pass. Mix at any ratio without re-rendering. Massive workflow speedup for look-dev.
---
## Sources
- [Three-Point Lighting with HDRI in Blender — MattePaint Academy](https://mattepaint.com/academy/tutorial/three-point-lighting-in-blender/)
- [Poly Haven — HDRIs (free, CC0)](https://polyhaven.com/hdris)
- [BlenderKit — Studio HDRI assets](https://www.blenderkit.com/asset-gallery-detail/62b625dc-59f2-4bc2-83da-d8518869cab9/)
- [Whizzy Studios — Cinematic lighting in Blender + Nuke](https://www.whizzystudios.com/post/from-concept-to-final-render-achieving-cinematic-lighting-and-composition-in-blender-and-nuke)
- [B3D Interplanety — Making your own studio HDRI](https://b3d.interplanety.org/en/making-your-own-studio-hdri-in-blender/)
- [Brandon3D — How to use HDRI in Blender 3D](https://brandon3d.com/hdri/)
- [Lightmap — HDR Light Studio Blender plugin](https://www.lightmap.co.uk/hdrlightstudio/connections/blender/)
---
## Outstanding
- [ ] Specific recipes by genre: portrait, food, jewelry, architecture
- [ ] Time-of-day presets (sunrise, noon, sunset, blue hour, night)
- [ ] Volumetric setups (god rays, fog)
- [ ] Practical lighting (matching scene reference photos)
SKILL.md
---
name: blender-lighting
description: Light Blender scenes professionally — three-point setups, HDRI environments, studio/cinematic/dramatic configurations, light groups, color temperature, soft vs hard shadows. Use whenever the user asks to "light the scene", "set up lighting", "make it look cinematic / dramatic / studio / outdoor / sunset", "add a key light", "use HDRI", or any lighting-related request. Make sure to use this skill even if the user does not say "light" — also covers "make it look professional", "studio shot", "moody atmosphere", "golden hour", "rim light". Pairs with blender-materials (lighting reveals materials) and blender-cameras (lighting + composition together = shot).
when_to_use: Any lighting setup or modification in Blender. Includes HDRI/environment lighting and individual lamp placement.
allowed-tools: Read Bash mcp__blender__execute_blender_code mcp__blender__get_scene_info mcp__blender__get_object_info
---
# 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:
```python
from 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
```python
from 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 | |
```python
import 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.
```python
import 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:
1. **Make the world background dark** (Strength 0.10–0.20). Otherwise the bulb's contribution is drowned out by ambient.
2. **Reduce or remove the standard 3-point fill/rim**. The practical light should dominate.
3. **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.
4. **Tune emission strength HIGH** for small mesh emitters (see `blender-materials` Recipe 11b — bulb spheres need Strength 800-3000 to read like real bulbs).
5. **Cycles `max_bounces` ≥ 16** for proper interior-shade lighting — the bulb's light needs to bounce inside the shade and out through the opening.
```python
import 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)
```python
import 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
```python
import 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](https://polyhaven.com/hdris) (CC0).
### Recipe 3 — Sunny outdoor
```python
import 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
```python
import 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
```python
import 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
```python
import 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.