You are participating in Blender-Bench, a public benchmark where AI models complete the same Blender task.

Your result will be compared head-to-head with another model. Anyone on the internet can vote based on two equally important published views: a 2D rendered presentation and an interactive 3D scene exported to GLB. Treat neither view as optional; make the result clear and compelling in both.

Complete the request as well as you can. The benchmark does not prescribe a workflow or visual style; use whichever Blender techniques you consider appropriate. Make the important requirements evident in the published result, save a complete scene, and present the work clearly in every requested output.

The 2D presentation is rendered by the benchmark from your saved scene using Cycles at 1536 x 1536, 64 samples, denoising enabled, and GPU acceleration when available with a CPU fallback. It is the native-fidelity view of the Blender scene, so you may use Blender's full feature set. Preview and validate your materials, lighting, color management, and camera composition in Cycles, and do not rely on EEVEE-only effects for the final appearance.

The interactive 3D view is a best-effort GLB export containing geometry, compatible materials, the contestant camera, and supported lights. Because glTF cannot represent every Blender feature, native effects such as particles, hair or curves, volumes, simulations, and some procedural geometry may appear only in the 2D render. The benchmark may also generate a separate preview GLB by baking supported procedural material inputs to textures; this does not convert unsupported geometry or guarantee exact parity with Blender. When practical, keep the core form and scene readable in the 3D view while using the 2D presentation to show native Blender features faithfully.

## Harness Instructions

You are writing a single Python script for Blender 5.1 using the bpy API. The scene starts empty. Your script must build everything the user's prompt asks for.

You are building a real three-dimensional scene, and evaluators judge the whole scene and your final render side by side — an interactive 3D view and a benchmark-generated 360° camera orbit rendered in Cycles from your saved scene appear next to your presentation render, so forms that only read correctly from one angle will show. Build genuine 3D first, then present it well: before you finish, set up a camera and lighting to frame a single, well-composed presentation image of your result — a good scene that happens to have a well-placed camera and lights. The final benchmark capture overrides the scene render settings and renders in Cycles at 1536x1536, 64 samples, with denoising and GPU acceleration (CPU fallback). Preview and validate materials, lighting, color management, and composition in Cycles; do not rely on EEVEE-only effects. Use whatever Blender features best serve the result; a best-effort GLB export is also provided for interactive inspection, and features glTF cannot represent are not expected to survive it.

You get one attempt: the script runs once, headless, with no feedback and no chance to iterate, so write robust code. Reply with exactly one fenced Python code block containing the complete script and nothing else.


## Task

Complete the following Blender task:

<task>

A classical head bust in the Greco-Roman tradition — the kind of plaster cast after the antique that sits in every drawing atelier — presented alone on a pedestal under studio light. This is a pure sculpting test: no costume, no scene, no story to hide behind. The face is the whole assignment.

The head. An idealized classical head in the manner of the great antique casts (a David, an Apollo, a Doryphoros — the specific reference is free, the idiom is not): calm, symmetric, youthful-heroic features with the canonical structure that atelier casts exist to teach. The planes of the face must be right — brow ridge stepping to the nose, clear cheekbone structure, the philtrum and the strong classical mouth, eyes with sculpted (not textured) iris-and-pupil suggestion or the blank antique eye — choose one convention and hold it. Hair in sculpted classical curls or waves with real clumped mass, not a smooth cap and not noise. The head sits on a neck cut off in the traditional bust termination — a clean geometric truncation into a small integral socle, or shoulders ending in the classic broken-edge cut.

Anatomy is judged mercilessly here. A slightly stylized but structurally correct head beats an uncanny near-likeness. Asymmetry from sculpting is human and fine; collapsed eye sockets, pasted-on ears, or a rubber jawline are not.

Material — plaster or marble, committed. Either: matte white atelier plaster with the subtlest subsurface scattering, faint seam lines from the casting mold, small chips at the socle edge, and a whisper of dust-grey in the recesses; or white statuary marble with soft SSS depth and delicate grey veining that follows the form. Pick one and make it convincing at close range. No pure-white shadeless material — the render must prove the material has depth.

Pedestal & setting. A simple column pedestal or plinth, a neutral studio backdrop, nothing else. One soft key light placed to rake the facial planes the way a drawing instructor would set it — form shadow down one side of the nose and cheek, a clear terminator, gentle bounce fill so the shadow side keeps its planes. The classic atelier lighting is part of the brief.

Composition. Camera at the bust's eye level, three-quarter view, head filling most of the frame. The final image should look like a photograph from a cast collection catalogue.

</task>


[System]
You are participating in Blender-Bench, a public benchmark where AI models complete the same Blender task.

Your result will be compared head-to-head with another model. Anyone on the internet can vote based on two equally important published views: a 2D rendered presentation and an interactive 3D scene exported to GLB. Treat neither view as optional; make the result clear and compelling in both.

Complete the request as well as you can. The benchmark does not prescribe a workflow or visual style; use whichever Blender techniques you consider appropriate. Make the important requirements evident in the published result, save a complete scene, and present the work clearly in every requested output.

The 2D presentation is rendered by the benchmark from your saved scene using Cycles at 1536 x 1536, 64 samples, denoising enabled, and GPU acceleration when available with a CPU fallback. It is the native-fidelity view of the Blender scene, so you may use Blender's full feature set. Preview and validate your materials, lighting, color management, and camera composition in Cycles, and do not rely on EEVEE-only effects for the final appearance.

The interactive 3D view is a best-effort GLB export containing geometry, compatible materials, the contestant camera, and supported lights. Because glTF cannot represent every Blender feature, native effects such as particles, hair or curves, volumes, simulations, and some procedural geometry may appear only in the 2D render. The benchmark may also generate a separate preview GLB by baking supported procedural material inputs to textures; this does not convert unsupported geometry or guarantee exact parity with Blender. When practical, keep the core form and scene readable in the 3D view while using the 2D presentation to show native Blender features faithfully.

## Harness Instructions

You are writing a single Python script for Blender 5.1 using the bpy API. The scene starts empty. Your script must build everything the user's prompt asks for.

You are building a real three-dimensional scene, and evaluators judge the whole scene and your final render side by side — an interactive 3D view and a benchmark-generated 360° camera orbit rendered in Cycles from your saved scene appear next to your presentation render, so forms that only read correctly from one angle will show. Build genuine 3D first, then present it well: before you finish, set up a camera and lighting to frame a single, well-composed presentation image of your result — a good scene that happens to have a well-placed camera and lights. The final benchmark capture overrides the scene render settings and renders in Cycles at 1536x1536, 64 samples, with denoising and GPU acceleration (CPU fallback). Preview and validate materials, lighting, color management, and composition in Cycles; do not rely on EEVEE-only effects. Use whatever Blender features best serve the result; a best-effort GLB export is also provided for interactive inspection, and features glTF cannot represent are not expected to survive it.

You get one attempt: the script runs once, headless, with no feedback and no chance to iterate, so write robust code. Reply with exactly one fenced Python code block containing the complete script and nothing else.

[User]
## Task

Complete the following Blender task:

<task>

A classical head bust in the Greco-Roman tradition — the kind of plaster cast after the antique that sits in every drawing atelier — presented alone on a pedestal under studio light. This is a pure sculpting test: no costume, no scene, no story to hide behind. The face is the whole assignment.

The head. An idealized classical head in the manner of the great antique casts (a David, an Apollo, a Doryphoros — the specific reference is free, the idiom is not): calm, symmetric, youthful-heroic features with the canonical structure that atelier casts exist to teach. The planes of the face must be right — brow ridge stepping to the nose, clear cheekbone structure, the philtrum and the strong classical mouth, eyes with sculpted (not textured) iris-and-pupil suggestion or the blank antique eye — choose one convention and hold it. Hair in sculpted classical curls or waves with real clumped mass, not a smooth cap and not noise. The head sits on a neck cut off in the traditional bust termination — a clean geometric truncation into a small integral socle, or shoulders ending in the classic broken-edge cut.

Anatomy is judged mercilessly here. A slightly stylized but structurally correct head beats an uncanny near-likeness. Asymmetry from sculpting is human and fine; collapsed eye sockets, pasted-on ears, or a rubber jawline are not.

Material — plaster or marble, committed. Either: matte white atelier plaster with the subtlest subsurface scattering, faint seam lines from the casting mold, small chips at the socle edge, and a whisper of dust-grey in the recesses; or white statuary marble with soft SSS depth and delicate grey veining that follows the form. Pick one and make it convincing at close range. No pure-white shadeless material — the render must prove the material has depth.

Pedestal & setting. A simple column pedestal or plinth, a neutral studio backdrop, nothing else. One soft key light placed to rake the facial planes the way a drawing instructor would set it — form shadow down one side of the nose and cheek, a clear terminator, gentle bounce fill so the shadow side keeps its planes. The classic atelier lighting is part of the brief.

Composition. Camera at the bust's eye level, three-quarter view, head filling most of the frame. The final image should look like a photograph from a cast collection catalogue.

</task>

[System]
You are participating in Blender-Bench, a public benchmark where AI models complete the same Blender task.

Your result will be compared head-to-head with another model. Anyone on the internet can vote based on two equally important published views: a 2D rendered presentation and an interactive 3D scene exported to GLB. Treat neither view as optional; make the result clear and compelling in both.

Complete the request as well as you can. The benchmark does not prescribe a workflow or visual style; use whichever Blender techniques you consider appropriate. Make the important requirements evident in the published result, save a complete scene, and present the work clearly in every requested output.

The 2D presentation is rendered by the benchmark from your saved scene using Cycles at 1536 x 1536, 64 samples, denoising enabled, and GPU acceleration when available with a CPU fallback. It is the native-fidelity view of the Blender scene, so you may use Blender's full feature set. Preview and validate your materials, lighting, color management, and camera composition in Cycles, and do not rely on EEVEE-only effects for the final appearance.

The interactive 3D view is a best-effort GLB export containing geometry, compatible materials, the contestant camera, and supported lights. Because glTF cannot represent every Blender feature, native effects such as particles, hair or curves, volumes, simulations, and some procedural geometry may appear only in the 2D render. The benchmark may also generate a separate preview GLB by baking supported procedural material inputs to textures; this does not convert unsupported geometry or guarantee exact parity with Blender. When practical, keep the core form and scene readable in the 3D view while using the 2D presentation to show native Blender features faithfully.

## Harness Instructions

You are writing a single Python script for Blender 5.1 using the bpy API. The scene starts empty. Your script must build everything the user's prompt asks for.

You are building a real three-dimensional scene, and evaluators judge the whole scene and your final render side by side — an interactive 3D view and a benchmark-generated 360° camera orbit rendered in Cycles from your saved scene appear next to your presentation render, so forms that only read correctly from one angle will show. Build genuine 3D first, then present it well: before you finish, set up a camera and lighting to frame a single, well-composed presentation image of your result — a good scene that happens to have a well-placed camera and lights. The final benchmark capture overrides the scene render settings and renders in Cycles at 1536x1536, 64 samples, with denoising and GPU acceleration (CPU fallback). Preview and validate materials, lighting, color management, and composition in Cycles; do not rely on EEVEE-only effects. Use whatever Blender features best serve the result; a best-effort GLB export is also provided for interactive inspection, and features glTF cannot represent are not expected to survive it.

You get one attempt: the script runs once, headless, with no feedback and no chance to iterate, so write robust code. Reply with exactly one fenced Python code block containing the complete script and nothing else.

[User]
## Task

Complete the following Blender task:

<task>

A classical head bust in the Greco-Roman tradition — the kind of plaster cast after the antique that sits in every drawing atelier — presented alone on a pedestal under studio light. This is a pure sculpting test: no costume, no scene, no story to hide behind. The face is the whole assignment.

The head. An idealized classical head in the manner of the great antique casts (a David, an Apollo, a Doryphoros — the specific reference is free, the idiom is not): calm, symmetric, youthful-heroic features with the canonical structure that atelier casts exist to teach. The planes of the face must be right — brow ridge stepping to the nose, clear cheekbone structure, the philtrum and the strong classical mouth, eyes with sculpted (not textured) iris-and-pupil suggestion or the blank antique eye — choose one convention and hold it. Hair in sculpted classical curls or waves with real clumped mass, not a smooth cap and not noise. The head sits on a neck cut off in the traditional bust termination — a clean geometric truncation into a small integral socle, or shoulders ending in the classic broken-edge cut.

Anatomy is judged mercilessly here. A slightly stylized but structurally correct head beats an uncanny near-likeness. Asymmetry from sculpting is human and fine; collapsed eye sockets, pasted-on ears, or a rubber jawline are not.

Material — plaster or marble, committed. Either: matte white atelier plaster with the subtlest subsurface scattering, faint seam lines from the casting mold, small chips at the socle edge, and a whisper of dust-grey in the recesses; or white statuary marble with soft SSS depth and delicate grey veining that follows the form. Pick one and make it convincing at close range. No pure-white shadeless material — the render must prove the material has depth.

Pedestal & setting. A simple column pedestal or plinth, a neutral studio backdrop, nothing else. One soft key light placed to rake the facial planes the way a drawing instructor would set it — form shadow down one side of the nose and cheek, a clear terminator, gentle bounce fill so the shadow side keeps its planes. The classic atelier lighting is part of the brief.

Composition. Camera at the bust's eye level, three-quarter view, head filling most of the frame. The final image should look like a photograph from a cast collection catalogue.

</task>

payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")

```python
import bpy
import math
import mathutils
import random

# -----------------------------------------------
# Utility functions
# -----------------------------------------------
def create_object(name, mesh):
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    return obj

def set_smooth(obj):
    for f in obj.data.polygons:
        f.use_smooth = True

def add_subdivision(obj, levels=2, render_levels=2):
    mod = obj.modifiers.new(name="Subdiv", type='SUBSURF')
    mod.levels = levels
    mod.render_levels = render_levels
    return mod

def apply_modifier(obj, mod_name):
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.modifier_apply(modifier=mod_name)

# Head shape function: returns radius for given (phi, theta) [phi: 0 top, pi bottom]
def head_radius(phi, theta):
    # Base ellipsoid
    rx = 0.85   # X half-width
    ry = 0.95   # Y half-depth (face direction)
    rz = 1.1    # Z half-height
    # Convert spherical to Cartesian unit vector
    x = math.sin(phi)*math.cos(theta)
    y = math.sin(phi)*math.sin(theta)
    z = math.cos(phi)
    # Scale to ellipsoid
    r = 1.0 / math.sqrt( (x/rx)**2 + (y/ry)**2 + (z/rz)**2 )
    # Add nose bump (front, near phi~0.5)
    if phi > 0.3 and phi < 0.8 and abs(theta) < 0.3:
        nose_strength = 0.12
        nose = nose_strength * (1 - abs(theta)/0.3) * (1 - abs(phi-0.55)/0.25)
        r += nose
    # Brow ridge
    if phi > 0.4 and phi < 0.55 and abs(theta) < 0.6:
        brow = 0.04 * (1 - abs(theta)/0.6) * (1 - abs(phi-0.48)/0.07)
        r += brow
    # Cheekbones (side protrusions around mid face)
    if phi > 0.45 and phi < 0.75 and abs(theta) > 0.6 and abs(theta) < 1.2:
        cheek = 0.06 * (1 - abs(theta-0.9)/0.3) * (1 - abs(phi-0.6)/0.15)
        r += cheek
    # Chin bump
    if phi > 0.8 and phi < 1.1 and abs(theta) < 0.4:
        chin = 0.05 * (1 - abs(theta)/0.4) * (1 - abs(phi-0.95)/0.15)
        r += chin
    return r

# -----------------------------------------------
# Scene setup
# -----------------------------------------------
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

# Render settings
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.samples = 64
scene.cycles.use_denoising = True

# -----------------------------------------------
# Head mesh
# -----------------------------------------------
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, segments=32, ring_count=24, location=(0,0,0))
head_obj = bpy.context.active_object
head_obj.name = "Head"
set_smooth(head_obj)

# Subdivide
subdiv = add_subdivision(head_obj, levels=2, render_levels=2)
apply_modifier(head_obj, "Subdiv")

# Get mesh data (after apply)
mesh = head_obj.data
verts = mesh.vertices

# Apply head shape to vertices
center = mathutils.Vector((0,0,0))
for v in verts:
    pos = v.co.copy()
    x, y, z = pos
    # Spherical angles
    r_vert = pos.length
    if r_vert < 1e-6:
        continue
    phi = math.acos(z / r_vert)
    theta = math.atan2(y, x)  # but we want theta=0 at front? We set front = +Y direction
    # In our sphere, front is +Y (theta=pi/2)
    theta = math.atan2(y, x)  # range -pi..pi
    # For front, we want theta=pi/2 (y>0). Normalize to -pi..pi
    # Convert so that front direction (y>0,x=0) => theta=0? Simpler: use original theta.
    # But our head_radius uses theta=0 for front. So we shift.
    # Front of sphere is +Y. In standard polar, theta=0 is +X. So we define theta_front = atan2(x, y)?? Actually, we want theta to be angle from front direction.
    # Let's compute angle from +Y axis in XY plane: theta_f = math.atan2(x, y)   (since atan2(x,y) gives angle from Y axis towards X)
    # That way, at front (x=0,y>0) => theta_f=0; at left (x>0,y=0) => theta_f=pi/2.
    theta_f = math.atan2(x, y)  # front = 0
    # Also map phi as is (0..pi)
    target_r = head_radius(phi, theta_f)
    # Scale position to target radius along same direction
    direction = pos.normalized()
    v.co = direction * target_r

# Add eye cavities (rough)
# Find vertices in eye area and depress
for v in verts:
    x, y, z = v.co
    # Eye left (x positive?) Face front +Y
    # For left eye: x ~ +0.25, y ~ 0.7, z ~ 0.35
    dist_left = math.sqrt((x-0.25)**2 + (y-0.7)**2 + (z-0.35)**2)
    dist_right = math.sqrt((x+0.25)**2 + (y-0.7)**2 + (z-0.35)**2)
    if dist_left < 0.18:
        v.co.z -= (0.18 - dist_left) * 0.15
    if dist_right < 0.18:
        v.co.z -= (0.18 - dist_right) * 0.15

# -----------------------------------------------
# Eyes (blank)
# -----------------------------------------------
eye_material = bpy.data.materials.new(name="EyeWhite")
eye_material.use_nodes = True
eye_tree = eye_material.node_tree
eye_bsdf = eye_tree.nodes.get("Principled BSDF")
eye_bsdf.inputs['Base Color'].default_value = (1,1,1,1)
eye_bsdf.inputs['Roughness'].default_value = 0.1

def create_eye(loc):
    bpy.ops.mesh.primitive_uv_sphere_add(radius=0.08, location=loc)
    eye = bpy.context.active_object
    eye.name = "Eye"
    eye.data.materials.append(eye_material)
    set_smooth(eye)
    return eye

left_eye = create_eye((0.25, 0.7, 0.35))
right_eye = create_eye((-0.25, 0.7, 0.35))

# -----------------------------------------------
# Hair (clumps using boolean union of small spheres)
# -----------------------------------------------
# Create a base for hair on top and back
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.8, location=(0,0,0.6))
hair_base = bpy.context.active_object
hair_base.name = "HairBase"
set_smooth(hair_base)
# Remove bottom half
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.bisect(plane_co=(0,0,0), plane_no=(0,0,-1), clear_inner=True, clear_outer=False)
bpy.ops.object.mode_set(mode='OBJECT')

# Add many small spheres to create curls
random.seed(42)
for i in range(35):
    # Position on top of head (hemisphere radius ~0.85, but follow head shape)
    theta = random.uniform(0, 2*math.pi)
    phi = random.uniform(0, math.pi/2)  # top half
    r = 0.85 + random.uniform(-0.1, 0.1)
    # Convert to Cartesian
    x = r * math.sin(phi) * math.cos(theta)
    y = r * math.sin(phi) * math.sin(theta)
    z = r * math.cos(phi) + 0.2  # lift a bit
    # Place sphere
    bpy.ops.mesh.primitive_uv_sphere_add(radius=0.12, location=(x,y,z))
    curl = bpy.context.active_object
    curl.name = "Curl_%d" % i
    set_smooth(curl)
    # Random scale
    curl.scale = (random.uniform(0.6,1.5), random.uniform(0.6,1.5), random.uniform(0.8,1.8))
    bpy.ops.object.transform_apply(scale=True)
    # Join to hair base
    bpy.ops.object.select_all(action='DESELECT')
    hair_base.select_set(True)
    curl.select_set(True)
    bpy.context.view_layer.objects.active = hair_base
    bpy.ops.object.join()
    hair_base = bpy.context.active_object  # after join

# Apply a subdivision to smooth shape
add_subdivision(hair_base, levels=1, render_levels=1)
apply_modifier(hair_base, "Subdiv")

# Shrinkwrap to follow head shape (using head as target)
shrink = hair_base.modifiers.new(name="Shrinkwrap", type='SHRINKWRAP')
shrink.target = head_obj
shrink.wrap_method = 'NEAREST_SURFACEPOINT'
shrink.offset = 0.02
bpy.context.view_layer.objects.active = hair_base
bpy.ops.object.modifier_apply(modifier="Shrinkwrap")

# -----------------------------------------------
# Neck and socle (truncation)
# -----------------------------------------------
# Neck: cylinder extending down from head neck
bpy.ops.mesh.primitive_cylinder_add(vertices=24, radius=0.35, depth=0.6, location=(0,0,0.2))
neck = bpy.context.active_object
neck.name = "Neck"
set_smooth(neck)
# Place bottom at z = -0.4, top at z = 0.5 (to overlap head)
# Scale neck to fit head bottom
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0,0,-0.3))
bpy.ops.object.mode_set(mode='OBJECT')

# Socle: small box at bottom
bpy.ops.mesh.primitive_cube_add(size=0.5, location=(0,0,-0.8))
socle = bpy.context.active_object
socle.name = "Socle"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.resize(value=(0.6,0.6,0.08))
bpy.ops.object.mode_set(mode='OBJECT')

# Join neck and socle? Better keep separate for material

# -----------------------------------------------
# Pedestal (plinth)
# -----------------------------------------------
# Column
bpy.ops.mesh.primitive_cylinder_add(vertices=24, radius=0.4, depth=1.2, location=(0,0,-1.4))
pedestal_col = bpy.context.active_object
pedestal_col.name = "PedestalColumn"
set_smooth(pedestal_col)
# Base
bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=0.5, depth=0.15, location=(0,0,-2.0))
pedestal_base = bpy.context.active_object
pedestal_base.name = "PedestalBase"
set_smooth(pedestal_base)
# Top cap
bpy.ops.mesh.primitive_cylinder_add(vertices=24, radius=0.42, depth=0.1, location=(0,0,-0.7))
pedestal_top = bpy.context.active_object
pedestal_top.name = "PedestalTop"

# -----------------------------------------------
# Backdrop
# -----------------------------------------------
bpy.ops.mesh.primitive_plane_add(size=10, location=(0, -5, 1.5))
backdrop = bpy.context.active_object
backdrop.name = "Backdrop"
backdrop.rotation_euler = (0, 0, 0)

# -----------------------------------------------
# Materials
# -----------------------------------------------
# Marble material for head/neck
marble_mat = bpy.data.materials.new(name="Marble")
marble_mat.use_nodes = True
nodes = marble_mat.node_tree.nodes
links = marble_mat.node_tree.links
nodes.clear()

# Principled BSDF
bsdf = nodes.new('ShaderNodeBsdfPrincipled')
bsdf.inputs['Base Color'].default_value = (1,1,1,1)
bsdf.inputs['Roughness'].default_value = 0.25
bsdf.inputs['Subsurface'].default_value = 0.2
bsdf.inputs['Subsurface Color'].default_value = (1,1,1,1)
bsdf.inputs['Subsurface Radius'].default_value = (0.01,0.01,0.01)

# Add vein texture
tex = nodes.new('ShaderNodeTexNoise')
tex.inputs['Scale'].default_value = 2.0
tex.inputs['Detail'].default_value = 10.0
color_ramp = nodes.new('ShaderNodeValToRGB')
color_ramp.color_ramp.interpolation = 'LINEAR'
color_ramp.color_ramp.elements[0].color = (0.85,0.85,0.85,1)
color_ramp.color_ramp.elements[1].color = (1,1,1,1)
# Mix with base color
mix = nodes.new('ShaderNodeMixRGB')
mix.inputs['Fac'].default_value = 0.2
links.new(tex.outputs['Fac'], color_ramp.inputs['Fac'])
links.new(color_ramp.outputs['Color'], mix.inputs['Color1'])
mix.inputs['Color2'].default_value = (1,1,1,1)
links.new(mix.outputs['Color'], bsdf.inputs['Base Color'])

# Output
output = nodes.new('ShaderNodeOutputMaterial')
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

# Assign to head, neck, socle
head_obj.data.materials.append(marble_mat)
neck.data.materials.append(marble_mat)
socle.data.materials.append(marble_mat)

# Hair material (plaster-like, less SSS)
hair_mat = marble_mat.copy()
hair_mat.name = "HairPlaster"
# Remove SSS heavy, increase roughness
hair_nodes = hair_mat.node_tree.nodes
hair_bsdf = hair_nodes.get('Principled BSDF')
hair_bsdf.inputs['Subsurface'].default_value = 0.05
hair_bsdf.inputs['Roughness'].default_value = 0.6
hair_data = hair_nodes.get('Mix RGB')
if hair_data:
    hair_nodes.remove(hair_data)
# Reconnect texture directly to base color
tex_in = hair_nodes.get('Noise Texture')
ramp_in = hair_nodes.get('Val to RGB')
if tex_in and ramp_in:
    links.new(ramp_in.outputs['Color'], hair_bsdf.inputs['Base Color'])
else:
    hair_bsdf.inputs['Base Color'].default_value = (1,1,1,1)
hair_base.data.materials.append(hair_mat)

# Pedestal material (stone)
stone_mat = bpy.data.materials.new(name="Stone")
stone_mat.use_nodes = True
stone_nodes = stone_mat.node_tree.nodes
stone_nodes.clear()
s_bsdf = stone_nodes.new('ShaderNodeBsdfPrincipled')
s_bsdf.inputs['Base Color'].default_value = (0.6,0.58,0.55,1)
s_bsdf.inputs['Roughness'].default_value = 0.7
s_out = stone_nodes.new('ShaderNodeOutputMaterial')
stone_mat.node_tree.links.new(s_bsdf.outputs['BSDF'], s_out.inputs['Surface'])
pedestal_col.data.materials.append(stone_mat)
pedestal_base.data.materials.append(stone_mat)
pedestal_top.data.materials.append(stone_mat)

# Backdrop material
backdrop_mat = bpy.data.materials.new(name="Backdrop")
backdrop_mat.use_nodes = True
back_nodes = backdrop_mat.node_tree.nodes
back_nodes.clear()
b_bsdf = back_nodes.new('ShaderNodeBsdfPrincipled')
b_bsdf.inputs['Base Color'].default_value = (0.4,0.4,0.4,1)
b_bsdf.inputs['Roughness'].default_value = 0.8
b_out = back_nodes.new('ShaderNodeOutputMaterial')
backdrop_mat.node_tree.links.new(b_bsdf.outputs['BSDF'], b_out.inputs['Surface'])
backdrop.data.materials.append(backdrop_mat)

# -----------------------------------------------
# Lighting
# -----------------------------------------------
# Key light (area) - left side raking
bpy.ops.object.light_add(type='AREA', location=(-2.5, 2.5, 2.0))
key = bpy.context.active_object
key.name = "KeyLight"
key.data.energy = 600
key.data.size = 0.5
key.rotation_euler = (math.radians(60), 0, math.radians(45))

# Fill light (softer)
bpy.ops.object.light_add(type='AREA', location=(2.0, 1.5, 1.5))
fill = bpy.context.active_object
fill.name = "FillLight"
fill.data.energy = 200
fill.data.size = 1.5
fill.rotation_euler = (math.radians(70), 0, math.radians(-45))

# Rim light (optional)
bpy.ops.object.light_add(type='AREA', location=(0, -3, 2.5))
rim = bpy.context.active_object
rim.name = "RimLight"
rim.data.energy = 100
rim.data.size = 0.3
rim.rotation_euler = (math.radians(80), 0, 0)

# -----------------------------------------------
# Camera
# -----------------------------------------------
bpy.ops.object.camera_add(location=(1.8, -3.5, 1.6))
cam = bpy.context.active_object
cam.name = "Camera"
cam.rotation_euler = (math.radians(78), 0, math.radians(40))
scene.camera = cam
# Set camera to three-quarter view; we'll adjust via composition later.
# For now, lock camera to view? Not needed.

# -----------------------------------------------
# Final scene adjustments
# -----------------------------------------------
# Hide selection wireframes in render? Not needed.
# Ensure all objects are visible
for ob in bpy.data.objects:
    ob.hide_render = False
    ob.hide_viewport = False

# Optional: add a tiny bevel to socle and pedestal for realism
# (omitted for simplicity)

print("Scene build complete.")
```


```python
import bpy
import math
import mathutils
import random

# -----------------------------------------------
# Utility functions
# -----------------------------------------------
def create_object(name, mesh):
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)
    return obj

def set_smooth(obj):
    for f in obj.data.polygons:
        f.use_smooth = True

def add_subdivision(obj, levels=2, render_levels=2):
    mod = obj.modifiers.new(name="Subdiv", type='SUBSURF')
    mod.levels = levels
    mod.render_levels = render_levels
    return mod

def apply_modifier(obj, mod_name):
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.modifier_apply(modifier=mod_name)

# Head shape function: returns radius for given (phi, theta) [phi: 0 top, pi bottom]
def head_radius(phi, theta):
    # Base ellipsoid
    rx = 0.85   # X half-width
    ry = 0.95   # Y half-depth (face direction)
    rz = 1.1    # Z half-height
    # Convert spherical to Cartesian unit vector
    x = math.sin(phi)*math.cos(theta)
    y = math.sin(phi)*math.sin(theta)
    z = math.cos(phi)
    # Scale to ellipsoid
    r = 1.0 / math.sqrt( (x/rx)**2 + (y/ry)**2 + (z/rz)**2 )
    # Add nose bump (front, near phi~0.5)
    if phi > 0.3 and phi < 0.8 and abs(theta) < 0.3:
        nose_strength = 0.12
        nose = nose_strength * (1 - abs(theta)/0.3) * (1 - abs(phi-0.55)/0.25)
        r += nose
    # Brow ridge
    if phi > 0.4 and phi < 0.55 and abs(theta) < 0.6:
        brow = 0.04 * (1 - abs(theta)/0.6) * (1 - abs(phi-0.48)/0.07)
        r += brow
    # Cheekbones (side protrusions around mid face)
    if phi > 0.45 and phi < 0.75 and abs(theta) > 0.6 and abs(theta) < 1.2:
        cheek = 0.06 * (1 - abs(theta-0.9)/0.3) * (1 - abs(phi-0.6)/0.15)
        r += cheek
    # Chin bump
    if phi > 0.8 and phi < 1.1 and abs(theta) < 0.4:
        chin = 0.05 * (1 - abs(theta)/0.4) * (1 - abs(phi-0.95)/0.15)
        r += chin
    return r

# -----------------------------------------------
# Scene setup
# -----------------------------------------------
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

# Render settings
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.samples = 64
scene.cycles.use_denoising = True

# -----------------------------------------------
# Head mesh
# -----------------------------------------------
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, segments=32, ring_count=24, location=(0,0,0))
head_obj = bpy.context.active_object
head_obj.name = "Head"
set_smooth(head_obj)

# Subdivide
subdiv = add_subdivision(head_obj, levels=2, render_levels=2)
apply_modifier(head_obj, "Subdiv")

# Get mesh data (after apply)
mesh = head_obj.data
verts = mesh.vertices

# Apply head shape to vertices
center = mathutils.Vector((0,0,0))
for v in verts:
    pos = v.co.copy()
    x, y, z = pos
    # Spherical angles
    r_vert = pos.length
    if r_vert < 1e-6:
        continue
    phi = math.acos(z / r_vert)
    theta = math.atan2(y, x)  # but we want theta=0 at front? We set front = +Y direction
    # In our sphere, front is +Y (theta=pi/2)
    theta = math.atan2(y, x)  # range -pi..pi
    # For front, we want theta=pi/2 (y>0). Normalize to -pi..pi
    # Convert so that front direction (y>0,x=0) => theta=0? Simpler: use original theta.
    # But our head_radius uses theta=0 for front. So we shift.
    # Front of sphere is +Y. In standard polar, theta=0 is +X. So we define theta_front = atan2(x, y)?? Actually, we want theta to be angle from front direction.
    # Let's compute angle from +Y axis in XY plane: theta_f = math.atan2(x, y)   (since atan2(x,y) gives angle from Y axis towards X)
    # That way, at front (x=0,y>0) => theta_f=0; at left (x>0,y=0) => theta_f=pi/2.
    theta_f = math.atan2(x, y)  # front = 0
    # Also map phi as is (0..pi)
    target_r = head_radius(phi, theta_f)
    # Scale position to target radius along same direction
    direction = pos.normalized()
    v.co = direction * target_r

# Add eye cavities (rough)
# Find vertices in eye area and depress
for v in verts:
    x, y, z = v.co
    # Eye left (x positive?) Face front +Y
    # For left eye: x ~ +0.25, y ~ 0.7, z ~ 0.35
    dist_left = math.sqrt((x-0.25)**2 + (y-0.7)**2 + (z-0.35)**2)
    dist_right = math.sqrt((x+0.25)**2 + (y-0.7)**2 + (z-0.35)**2)
    if dist_left < 0.18:
        v.co.z -= (0.18 - dist_left) * 0.15
    if dist_right < 0.18:
        v.co.z -= (0.18 - dist_right) * 0.15

# -----------------------------------------------
# Eyes (blank)
# -----------------------------------------------
eye_material = bpy.data.materials.new(name="EyeWhite")
eye_material.use_nodes = True
eye_tree = eye_material.node_tree
eye_bsdf = eye_tree.nodes.get("Principled BSDF")
eye_bsdf.inputs['Base Color'].default_value = (1,1,1,1)
eye_bsdf.inputs['Roughness'].default_value = 0.1

def create_eye(loc):
    bpy.ops.mesh.primitive_uv_sphere_add(radius=0.08, location=loc)
    eye = bpy.context.active_object
    eye.name = "Eye"
    eye.data.materials.append(eye_material)
    set_smooth(eye)
    return eye

left_eye = create_eye((0.25, 0.7, 0.35))
right_eye = create_eye((-0.25, 0.7, 0.35))

# -----------------------------------------------
# Hair (clumps using boolean union of small spheres)
# -----------------------------------------------
# Create a base for hair on top and back
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.8, location=(0,0,0.6))
hair_base = bpy.context.active_object
hair_base.name = "HairBase"
set_smooth(hair_base)
# Remove bottom half
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.bisect(plane_co=(0,0,0), plane_no=(0,0,-1), clear_inner=True, clear_outer=False)
bpy.ops.object.mode_set(mode='OBJECT')

# Add many small spheres to create curls
random.seed(42)
for i in range(35):
    # Position on top of head (hemisphere radius ~0.85, but follow head shape)
    theta = random.uniform(0, 2*math.pi)
    phi = random.uniform(0, math.pi/2)  # top half
    r = 0.85 + random.uniform(-0.1, 0.1)
    # Convert to Cartesian
    x = r * math.sin(phi) * math.cos(theta)
    y = r * math.sin(phi) * math.sin(theta)
    z = r * math.cos(phi) + 0.2  # lift a bit
    # Place sphere
    bpy.ops.mesh.primitive_uv_sphere_add(radius=0.12, location=(x,y,z))
    curl = bpy.context.active_object
    curl.name = "Curl_%d" % i
    set_smooth(curl)
    # Random scale
    curl.scale = (random.uniform(0.6,1.5), random.uniform(0.6,1.5), random.uniform(0.8,1.8))
    bpy.ops.object.transform_apply(scale=True)
    # Join to hair base
    bpy.ops.object.select_all(action='DESELECT')
    hair_base.select_set(True)
    curl.select_set(True)
    bpy.context.view_layer.objects.active = hair_base
    bpy.ops.object.join()
    hair_base = bpy.context.active_object  # after join

# Apply a subdivision to smooth shape
add_subdivision(hair_base, levels=1, render_levels=1)
apply_modifier(hair_base, "Subdiv")

# Shrinkwrap to follow head shape (using head as target)
shrink = hair_base.modifiers.new(name="Shrinkwrap", type='SHRINKWRAP')
shrink.target = head_obj
shrink.wrap_method = 'NEAREST_SURFACEPOINT'
shrink.offset = 0.02
bpy.context.view_layer.objects.active = hair_base
bpy.ops.object.modifier_apply(modifier="Shrinkwrap")

# -----------------------------------------------
# Neck and socle (truncation)
# -----------------------------------------------
# Neck: cylinder extending down from head neck
bpy.ops.mesh.primitive_cylinder_add(vertices=24, radius=0.35, depth=0.6, location=(0,0,0.2))
neck = bpy.context.active_object
neck.name = "Neck"
set_smooth(neck)
# Place bottom at z = -0.4, top at z = 0.5 (to overlap head)
# Scale neck to fit head bottom
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0,0,-0.3))
bpy.ops.object.mode_set(mode='OBJECT')

# Socle: small box at bottom
bpy.ops.mesh.primitive_cube_add(size=0.5, location=(0,0,-0.8))
socle = bpy.context.active_object
socle.name = "Socle"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.resize(value=(0.6,0.6,0.08))
bpy.ops.object.mode_set(mode='OBJECT')

# Join neck and socle? Better keep separate for material

# -----------------------------------------------
# Pedestal (plinth)
# -----------------------------------------------
# Column
bpy.ops.mesh.primitive_cylinder_add(vertices=24, radius=0.4, depth=1.2, location=(0,0,-1.4))
pedestal_col = bpy.context.active_object
pedestal_col.name = "PedestalColumn"
set_smooth(pedestal_col)
# Base
bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=0.5, depth=0.15, location=(0,0,-2.0))
pedestal_base = bpy.context.active_object
pedestal_base.name = "PedestalBase"
set_smooth(pedestal_base)
# Top cap
bpy.ops.mesh.primitive_cylinder_add(vertices=24, radius=0.42, depth=0.1, location=(0,0,-0.7))
pedestal_top = bpy.context.active_object
pedestal_top.name = "PedestalTop"

# -----------------------------------------------
# Backdrop
# -----------------------------------------------
bpy.ops.mesh.primitive_plane_add(size=10, location=(0, -5, 1.5))
backdrop = bpy.context.active_object
backdrop.name = "Backdrop"
backdrop.rotation_euler = (0, 0, 0)

# -----------------------------------------------
# Materials
# -----------------------------------------------
# Marble material for head/neck
marble_mat = bpy.data.materials.new(name="Marble")
marble_mat.use_nodes = True
nodes = marble_mat.node_tree.nodes
links = marble_mat.node_tree.links
nodes.clear()

# Principled BSDF
bsdf = nodes.new('ShaderNodeBsdfPrincipled')
bsdf.inputs['Base Color'].default_value = (1,1,1,1)
bsdf.inputs['Roughness'].default_value = 0.25
bsdf.inputs['Subsurface'].default_value = 0.2
bsdf.inputs['Subsurface Color'].default_value = (1,1,1,1)
bsdf.inputs['Subsurface Radius'].default_value = (0.01,0.01,0.01)

# Add vein texture
tex = nodes.new('ShaderNodeTexNoise')
tex.inputs['Scale'].default_value = 2.0
tex.inputs['Detail'].default_value = 10.0
color_ramp = nodes.new('ShaderNodeValToRGB')
color_ramp.color_ramp.interpolation = 'LINEAR'
color_ramp.color_ramp.elements[0].color = (0.85,0.85,0.85,1)
color_ramp.color_ramp.elements[1].color = (1,1,1,1)
# Mix with base color
mix = nodes.new('ShaderNodeMixRGB')
mix.inputs['Fac'].default_value = 0.2
links.new(tex.outputs['Fac'], color_ramp.inputs['Fac'])
links.new(color_ramp.outputs['Color'], mix.inputs['Color1'])
mix.inputs['Color2'].default_value = (1,1,1,1)
links.new(mix.outputs['Color'], bsdf.inputs['Base Color'])

# Output
output = nodes.new('ShaderNodeOutputMaterial')
links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

# Assign to head, neck, socle
head_obj.data.materials.append(marble_mat)
neck.data.materials.append(marble_mat)
socle.data.materials.append(marble_mat)

# Hair material (plaster-like, less SSS)
hair_mat = marble_mat.copy()
hair_mat.name = "HairPlaster"
# Remove SSS heavy, increase roughness
hair_nodes = hair_mat.node_tree.nodes
hair_bsdf = hair_nodes.get('Principled BSDF')
hair_bsdf.inputs['Subsurface'].default_value = 0.05
hair_bsdf.inputs['Roughness'].default_value = 0.6
hair_data = hair_nodes.get('Mix RGB')
if hair_data:
    hair_nodes.remove(hair_data)
# Reconnect texture directly to base color
tex_in = hair_nodes.get('Noise Texture')
ramp_in = hair_nodes.get('Val to RGB')
if tex_in and ramp_in:
    links.new(ramp_in.outputs['Color'], hair_bsdf.inputs['Base Color'])
else:
    hair_bsdf.inputs['Base Color'].default_value = (1,1,1,1)
hair_base.data.materials.append(hair_mat)

# Pedestal material (stone)
stone_mat = bpy.data.materials.new(name="Stone")
stone_mat.use_nodes = True
stone_nodes = stone_mat.node_tree.nodes
stone_nodes.clear()
s_bsdf = stone_nodes.new('ShaderNodeBsdfPrincipled')
s_bsdf.inputs['Base Color'].default_value = (0.6,0.58,0.55,1)
s_bsdf.inputs['Roughness'].default_value = 0.7
s_out = stone_nodes.new('ShaderNodeOutputMaterial')
stone_mat.node_tree.links.new(s_bsdf.outputs['BSDF'], s_out.inputs['Surface'])
pedestal_col.data.materials.append(stone_mat)
pedestal_base.data.materials.append(stone_mat)
pedestal_top.data.materials.append(stone_mat)

# Backdrop material
backdrop_mat = bpy.data.materials.new(name="Backdrop")
backdrop_mat.use_nodes = True
back_nodes = backdrop_mat.node_tree.nodes
back_nodes.clear()
b_bsdf = back_nodes.new('ShaderNodeBsdfPrincipled')
b_bsdf.inputs['Base Color'].default_value = (0.4,0.4,0.4,1)
b_bsdf.inputs['Roughness'].default_value = 0.8
b_out = back_nodes.new('ShaderNodeOutputMaterial')
backdrop_mat.node_tree.links.new(b_bsdf.outputs['BSDF'], b_out.inputs['Surface'])
backdrop.data.materials.append(backdrop_mat)

# -----------------------------------------------
# Lighting
# -----------------------------------------------
# Key light (area) - left side raking
bpy.ops.object.light_add(type='AREA', location=(-2.5, 2.5, 2.0))
key = bpy.context.active_object
key.name = "KeyLight"
key.data.energy = 600
key.data.size = 0.5
key.rotation_euler = (math.radians(60), 0, math.radians(45))

# Fill light (softer)
bpy.ops.object.light_add(type='AREA', location=(2.0, 1.5, 1.5))
fill = bpy.context.active_object
fill.name = "FillLight"
fill.data.energy = 200
fill.data.size = 1.5
fill.rotation_euler = (math.radians(70), 0, math.radians(-45))

# Rim light (optional)
bpy.ops.object.light_add(type='AREA', location=(0, -3, 2.5))
rim = bpy.context.active_object
rim.name = "RimLight"
rim.data.energy = 100
rim.data.size = 0.3
rim.rotation_euler = (math.radians(80), 0, 0)

# -----------------------------------------------
# Camera
# -----------------------------------------------
bpy.ops.object.camera_add(location=(1.8, -3.5, 1.6))
cam = bpy.context.active_object
cam.name = "Camera"
cam.rotation_euler = (math.radians(78), 0, math.radians(40))
scene.camera = cam
# Set camera to three-quarter view; we'll adjust via composition later.
# For now, lock camera to view? Not needed.

# -----------------------------------------------
# Final scene adjustments
# -----------------------------------------------
# Hide selection wireframes in render? Not needed.
# Ensure all objects are visible
for ob in bpy.data.objects:
    ob.hide_render = False
    ob.hide_viewport = False

# Optional: add a tiny bevel to socle and pedestal for realism
# (omitted for simplicity)

print("Scene build complete.")
```