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>

An abandoned village slowly reclaimed by nature. Weathered houses with rusted, corroded metal roofs; overgrown foliage creeping up the walls and through broken windows; moss, grass, and vines spreading across the ground and over the rooftops. Convey age, decay, and the quiet return of the wild.

</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>

An abandoned village slowly reclaimed by nature. Weathered houses with rusted, corroded metal roofs; overgrown foliage creeping up the walls and through broken windows; moss, grass, and vines spreading across the ground and over the rooftops. Convey age, decay, and the quiet return of the wild.

</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>

An abandoned village slowly reclaimed by nature. Weathered houses with rusted, corroded metal roofs; overgrown foliage creeping up the walls and through broken windows; moss, grass, and vines spreading across the ground and over the rooftops. Convey age, decay, and the quiet return of the wild.

</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 mathutils
import math
import random

# -------------------------------------------------------------------
# Utility functions
# -------------------------------------------------------------------

def clear_scene():
    """Delete all objects, collections, and data blocks."""
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=False)
    # Also remove any leftover meshes, materials, etc.
    for block in bpy.data.meshes:
        bpy.data.meshes.remove(block)
    for block in bpy.data.materials:
        bpy.data.materials.remove(block)
    for block in bpy.data.textures:
        bpy.data.textures.remove(block)
    for block in bpy.data.images:
        bpy.data.images.remove(block)
    for block in bpy.data.curves:
        bpy.data.curves.remove(block)
    for block in bpy.data.lights:
        bpy.data.lights.remove(block)
    for block in bpy.data.cameras:
        bpy.data.cameras.remove(block)
    collections = list(bpy.data.collections)
    for coll in collections:
        bpy.data.collections.remove(coll)

def new_material(name, use_nodes=True):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = use_nodes
    return mat

def add_texture_coordinate(mat):
    nodes = mat.node_tree.nodes
    tc = nodes.new(type='ShaderNodeTexCoord')
    return tc

def add_mapping(mat):
    nodes = mat.node_tree.nodes
    mapping = nodes.new(type='ShaderNodeMapping')
    return mapping

# -------------------------------------------------------------------
# Material creation
# -------------------------------------------------------------------

def make_wood_material():
    """Weathered wood with grain and color variation."""
    mat = new_material("WeatheredWood")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links

    # Clear default nodes
    for node in nodes:
        nodes.remove(node)

    # Principled BSDF
    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    # Output
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    # Texture coordinates and mapping
    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    mapping.inputs['Scale'].default_value = (0.5, 0.5, 0.5)
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Wood grain wave texture
    wave = nodes.new(type='ShaderNodeTexWave')
    wave.wave_type = 'BANDS'
    wave.inputs['Scale'].default_value = 2.0
    wave.inputs['Distortion'].default_value = 3.0
    links.new(mapping.outputs['Vector'], wave.inputs['Vector'])

    # Color ramp for wood tones
    ramp = nodes.new(type='ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].color = (0.2, 0.1, 0.05, 1)
    ramp.color_ramp.elements[0].position = 0.0
    ramp.color_ramp.elements[1].color = (0.55, 0.35, 0.15, 1)
    ramp.color_ramp.elements[1].position = 1.0
    links.new(wave.outputs['Fac'], ramp.inputs['Fac'])

    # Mix with noise for weathering
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 5.0
    noise.inputs['Detail'].default_value = 5.0
    noise.inputs['Roughness'].default_value = 0.7
    mix = nodes.new(type='ShaderNodeMixRGB')
    mix.blend_type = 'MIX'
    mix.inputs['Fac'].default_value = 0.3
    links.new(ramp.outputs['Color'], mix.inputs['Color1'])
    links.new(noise.outputs['Color'], mix.inputs['Color2'])

    # Bump map
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.3
    links.new(noise.outputs['Fac'], bump.inputs['Height'])

    # Connect to principled
    links.new(mix.outputs['Color'], principled.inputs['Base Color'])
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

def make_metal_rust_material():
    """Rusted corrugated metal roof."""
    mat = new_material("RustedMetal")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    mapping.inputs['Scale'].default_value = (0.8, 0.8, 0.8)
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Base metal color
    base_metal = (0.3, 0.3, 0.35, 1)

    # Rust noise
    noise_rust = nodes.new(type='ShaderNodeTexNoise')
    noise_rust.inputs['Scale'].default_value = 3.0
    noise_rust.inputs['Detail'].default_value = 6.0
    noise_rust.inputs['Roughness'].default_value = 0.5

    mix_rust = nodes.new(type='ShaderNodeMixRGB')
    mix_rust.blend_type = 'MIX'
    # Colors: metal then rust
    color_metal = (0.4, 0.4, 0.45, 1)
    color_rust = (0.6, 0.15, 0.05, 1)
    links.new(noise_rust.outputs['Fac'], mix_rust.inputs['Fac'])
    mix_rust.inputs['Color1'].default_value = color_metal
    mix_rust.inputs['Color2'].default_value = color_rust
    links.new(tc.outputs['Object'], mix_rust.inputs['Vector'])  # not needed

    # Corrugation using wave texture
    wave = nodes.new(type='ShaderNodeTexWave')
    wave.wave_type = 'BANDS'
    wave.inputs['Scale'].default_value = 0.5
    wave.inputs['Distortion'].default_value = 2.0
    # Multiply wave with rust mix for variation
    mult = nodes.new(type='ShaderNodeMixRGB')
    mult.blend_type = 'MULTIPLY'
    links.new(wave.outputs['Fac'], mult.inputs['Fac'])
    links.new(mix_rust.outputs['Color'], mult.inputs['Color1'])
    links.new(mix_rust.outputs['Color'], mult.inputs['Color2'])

    # Roughness map from noise
    noise_rough = nodes.new(type='ShaderNodeTexNoise')
    noise_rough.inputs['Scale'].default_value = 2.0
    roughness_ramp = nodes.new(type='ShaderNodeValToRGB')
    roughness_ramp.color_ramp.elements[0].color = (0.6, 0.6, 0.6, 1)
    roughness_ramp.color_ramp.elements[0].position = 0.0
    roughness_ramp.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1)
    roughness_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise_rough.outputs['Fac'], roughness_ramp.inputs['Fac'])

    # Normal map for dents
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.5
    links.new(noise_rough.outputs['Fac'], bump.inputs['Height'])

    # Connect
    links.new(mult.outputs['Color'], principled.inputs['Base Color'])
    links.new(roughness_ramp.outputs['Color'], principled.inputs['Roughness'])
    principled.inputs['Metallic'].default_value = 0.8
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

def make_moss_material():
    """Moss for ground and walls."""
    mat = new_material("Moss")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    mapping.inputs['Scale'].default_value = (2.0, 2.0, 2.0)
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Base green
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 2.0
    noise.inputs['Detail'].default_value = 8.0
    color_ramp = nodes.new(type='ShaderNodeValToRGB')
    color_ramp.color_ramp.elements[0].color = (0.1, 0.4, 0.1, 1)
    color_ramp.color_ramp.elements[0].position = 0.0
    color_ramp.color_ramp.elements[1].color = (0.3, 0.6, 0.2, 1)
    color_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise.outputs['Fac'], color_ramp.inputs['Fac'])

    # Bump
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.8
    links.new(noise.outputs['Fac'], bump.inputs['Height'])

    links.new(color_ramp.outputs['Color'], principled.inputs['Base Color'])
    principled.inputs['Roughness'].default_value = 0.9
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

def make_grass_material():
    """Grass blade material (transparent planes)."""
    mat = new_material("Grass")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    # Color variation
    tc = nodes.new(type='ShaderNodeTexCoord')
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 3.0
    color_ramp = nodes.new(type='ShaderNodeValToRGB')
    color_ramp.color_ramp.elements[0].color = (0.2, 0.6, 0.1, 1)
    color_ramp.color_ramp.elements[0].position = 0.0
    color_ramp.color_ramp.elements[1].color = (0.4, 0.8, 0.2, 1)
    color_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise.outputs['Fac'], color_ramp.inputs['Fac'])
    links.new(tc.outputs['UV'], noise.inputs['Vector'])

    # Transparency (alpha from noise)
    # We'll use a separate texture for alpha to create blade shape
    # But for simplicity, use a wave texture to create vertical stripes
    wave = nodes.new(type='ShaderNodeTexWave')
    wave.wave_type = 'BANDS'
    wave.inputs['Scale'].default_value = 10.0
    # Map to UV along height
    # Add map range to concentrate alpha at top?
    # Alternatively, use a simple gradient
    gradient = nodes.new(type='ShaderNodeTexGradient')
    gradient.gradient_type = 'LINEAR'
    musgrave = nodes.new(type='ShaderNodeTexMusgrave')
    musgrave.inputs['Scale'].default_value = 5.0
    # Mix for random blade shapes
    mix = nodes.new(type='ShaderNodeMixRGB')
    mix.blend_type = 'MULTIPLY'
    links.new(gradient.outputs['Color'], mix.inputs['Color1'])
    links.new(musgrave.outputs['Fac'], mix.inputs['Color2'])
    links.new(tc.outputs['UV'], gradient.inputs['Vector'])
    links.new(tc.outputs['UV'], musgrave.inputs['Vector'])

    links.new(color_ramp.outputs['Color'], principled.inputs['Base Color'])
    links.new(mix.outputs['Color'], principled.inputs['Alpha'])
    principled.inputs['Roughness'].default_value = 0.7

    # Enable transparency
    mat.blend_method = 'BLEND'
    mat.shadow_method = 'HASHED'

    return mat

def make_vine_material():
    """Vine material with leaves."""
    mat = new_material("Vine")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    # Stem and leaf color mix
    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Noise for leaf clusters
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 2.0
    color_ramp = nodes.new(type='ShaderNodeValToRGB')
    color_ramp.color_ramp.elements[0].color = (0.3, 0.5, 0.1, 1)  # leaf
    color_ramp.color_ramp.elements[0].position = 0.0
    color_ramp.color_ramp.elements[1].color = (0.05, 0.02, 0.01, 1)  # stem
    color_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise.outputs['Fac'], color_ramp.inputs['Fac'])

    # Bump
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.4
    links.new(noise.outputs['Fac'], bump.inputs['Height'])

    links.new(color_ramp.outputs['Color'], principled.inputs['Base Color'])
    principled.inputs['Roughness'].default_value = 0.8
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

# -------------------------------------------------------------------
# Geometry creation
# -------------------------------------------------------------------

def create_ground(size=30, subdivisions=50):
    """Create a slightly hilly ground plane."""
    bpy.ops.mesh.primitive_grid_add(x_subdivisions=subdivisions, y_subdivisions=subdivisions, size=size, location=(0,0,0))
    ground = bpy.context.object
    ground.name = "Ground"

    # Displace with noise for gentle hills
    displace = ground.modifiers.new(name='Displace', type='DISPLACE')
    tex = bpy.data.textures.new('GroundNoise', type='CLOUDS')
    tex.noise_scale = 2.0
    tex.noise_depth = 2
    displace.texture = tex
    displace.strength = 0.5
    displace.mid_level = 0.0

    # Apply to have actual geometry for scattering later (or keep as modifier)
    # We'll keep it as modifier because we will apply later? Not needed for GN scatter.
    # Add material
    ground_material = new_material("Ground")
    nodes = ground_material.node_tree.nodes
    nodes.clear()
    # Use a simple mix of dirt and grass
    principled = nodes.new('ShaderNodeBsdfPrincipled')
    output = nodes.new('ShaderNodeOutputMaterial')
    nodes.new('ShaderNodeTexCoord')
    nodes.new('ShaderNodeMapping')
    # We'll create a quick node setup for ground
    # For brevity, assign a green-brown color
    principled.inputs['Base Color'].default_value = (0.3, 0.25, 0.15, 1)
    principled.inputs['Roughness'].default_value = 1.0
    ground_material.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
    ground.data.materials.append(ground_material)

    return ground

def create_house(location, rotation=(0,0,0), scale=(1,1,1), house_id=1):
    """Create a weathered house with metal roof."""
    # Container collection for this house
    coll = bpy.data.collections.new(f"House_{house_id}")
    bpy.context.scene.collection.children.link(coll)

    # Dimensions
    width = 2.5 * scale[0]
    depth = 2.0 * scale[1]
    height = 1.8 * scale[2]

    # Walls
    bpy.ops.mesh.primitive_cube_add(size=1, location=(0,0,0))
    walls = bpy.context.object
    walls.name = f"Walls_{house_id}"
    walls.scale = (width, depth, height)
    walls.location = (location[0], location[1], location[2] + height/2)
    coll.objects.link(walls)
    bpy.context.collection.objects.unlink(walls)
    walls.data.materials.append(make_wood_material())

    # Roof (prism shape)
    # We'll create a simple triangular roof using two planes angled, or use a cube and cut
    # Simpler: use a cube scaled and rotated
    roof_height = 0.6 * scale[2]
    roof_peak = location[2] + height + roof_height/2
    # Create a pyramid-like shape: use a triangle extruded
    # Use mesh from scratch
    roof_verts = [
        (-width/2, -depth/2, 0),
        (width/2, -depth/2, 0),
        (width/2, depth/2, 0),
        (-width/2, depth/2, 0),
        (0, -depth/2, roof_height),
        (0, depth/2, roof_height)
    ]
    roof_faces = [
        (0, 1, 4),
        (1, 2, 5, 4),
        (2, 3, 5),
        (3, 0, 4, 5)
    ]
    roof_mesh = bpy.data.meshes.new(f"Roof_{house_id}")
    roof_mesh.from_pydata(roof_verts, [], roof_faces)
    roof_mesh.update()
    roof_obj = bpy.data.objects.new(f"Roof_{house_id}", roof_mesh)
    roof_obj.location = (location[0], location[1], location[2] + height)
    coll.objects.link(roof_obj)
    bpy.context.collection.objects.unlink(roof_obj)
    roof_obj.data.materials.append(make_metal_rust_material())

    # Doors and windows - add as simple boolean cuts or separate geometry
    # For simplicity, add broken window frames with glass shards
    # We'll use a few planes with transparency for glass shards
    # Window positions
    windows = []
    # Front window
    windows.append((0, depth/2 * 0.8, height*0.5, 0.4, 0.5))
    # Side windows
    windows.append((width/2 * 0.7, 0, height*0.5, 0.4, 0.5))
    windows.append((-width/2 * 0.7, 0, height*0.5, 0.4, 0.5))
    # Back window
    windows.append((0, -depth/2 * 0.8, height*0.5, 0.4, 0.5))

    for (wx, wy, wz, ww, wh) in windows:
        # Create a flat plane for window opening (cutout) or just glass shards
        # We'll add broken glass shards (small triangles)
        shard_material = new_material(f"GlassShard_{house_id}_{wx}")
        shard_material.use_nodes = True
        nodes = shard_material.node_tree.nodes
        for n in nodes:
            nodes.remove(n)
        glass = nodes.new('ShaderNodeBsdfGlass')
        glass.inputs['Color'].default_value = (0.8, 0.9, 1.0, 1)
        glass.inputs['Roughness'].default_value = 0.2
        output = nodes.new('ShaderNodeOutputMaterial')
        shard_material.node_tree.links.new(glass.outputs['BSDF'], output.inputs['Surface'])

        # Create a few shard planes
        for _ in range(random.randint(3,6)):
            sx = wx + random.uniform(-0.15, 0.15)
            sy = wy + random.uniform(-0.15, 0.15)
            sz = wz + random.uniform(-0.15, 0.15)
            bpy.ops.mesh.primitive_plane_add(size=random.uniform(0.05, 0.15), location=(location[0]+sx, location[1]+sy, location[2]+sz))
            shard = bpy.context.object
            shard.name = f"Shard_{house_id}"
            coll.objects.link(shard)
            bpy.context.collection.objects.unlink(shard)
            shard.data.materials.append(shard_material)
            # Random rotation
            shard.rotation_euler = (random.random()*2*math.pi, random.random()*2*math.pi, random.random()*2*math.pi)

    # Add some vines climbing the walls using curves
    # We'll create a curve with a vine material and use geometry nodes to distribute leaves?
    # For simplicity, we'll create a few curve paths with circular bevel.
    vine_material = make_vine_material()
    for side in range(4):
        if side == 0: # front
            start = (location[0], location[1] + depth/2, location[2])
            end = (location[0], location[1] + depth/2, location[2] + height*0.8)
        elif side == 1: # right
            start = (location[0] + width/2, location[1], location[2])
            end = (location[0] + width/2, location[1], location[2] + height*0.6)
        elif side == 2: # back
            start = (location[0], location[1] - depth/2, location[2])
            end = (location[0], location[1] - depth/2, location[2] + height*0.7)
        else: # left
            start = (location[0] - width/2, location[1], location[2])
            end = (location[0] - width/2, location[1], location[2] + height*0.5)
        # Create curve
        curve_data = bpy.data.curves.new(f"VineCurve_{house_id}_{side}", 'CURVE')
        curve_data.dimensions = '3D'
        curve_data.fill_mode = 'FULL'
        curve_data.bevel_depth = 0.04
        curve_data.bevel_resolution = 4
        spline = curve_data.splines.new('BEZIER')
        spline.bezier_points.add(1)
        spline.bezier_points[0].co = start
        spline.bezier_points[1].co = end
        # Add some control points for wavy shape
        # We'll just use two points for simplicity
        curve_obj = bpy.data.objects.new(f"Vine_{house_id}_{side}", curve_data)
        coll.objects.link(curve_obj)
        bpy.context.collection.objects.unlink(curve_obj)
        curve_obj.data.materials.append(vine_material)

    # Add moss patches on walls and roof using geometry nodes
    # We'll create a geometry node group that scatters moss on faces
    # Alternatively, we can use a particle system, but geometry nodes are simpler to script
    # We'll apply a geometry nodes modifier to the house walls (and roof) that instances moss planes on faces
    # For the roof, we'll also add rusted patches via material already, but moss is physical.

    # Create a moss instance object: a small plane or cube with moss material
    bpy.ops.mesh.primitive_plane_add(size=0.1, location=(0,0,0))
    moss_plane = bpy.context.object
    moss_plane.name = f"MossPlane_{house_id}"
    moss_plane.data.materials.append(make_moss_material())
    coll.objects.link(moss_plane)
    bpy.context.collection.objects.unlink(moss_plane)
    moss_plane.hide_viewport = True
    moss_plane.hide_render = True  # hide original, only used as instance

    # Geometry nodes on walls
    gn_mod = walls.modifiers.new(name="MossScatter", type='NODES')
    # Create geometry node group
    node_group = bpy.data.node_groups.new(name="MossScatterGN", type='GeometryNodeTree')
    node_group.inputs.new('NodeSocketGeometry', 'Geometry')
    node_group.outputs.new('NodeSocketGeometry', 'Geometry')

    group_nodes = node_group.nodes
    group_links = node_group.links
    group_nodes.clear()

    # Input node
    input_node = group_nodes.new(type='NodeGroupInput')
    input_node.location = (-200, 0)
    # Output node
    output_node = group_nodes.new(type='NodeGroupOutput')
    output_node.location = (400, 0)

    # Distribute points on faces
    distribute = group_nodes.new(type='GeometryNodeDistributePointsOnFaces')
    distribute.distribute_method = 'RANDOM'
    distribute.inputs['Density'].default_value = 200
    distribute.inputs['Seed'].default_value = house_id * 100 + side
    # Add scale random
    random_val = group_nodes.new(type='FunctionNodeRandomValue')
    random_val.data_type = 'FLOAT'
    random_val.inputs['Min'].default_value = 0.5
    random_val.inputs['Max'].default_value = 1.5

    # Instance on points
    instance = group_nodes.new(type='GeometryNodeInstanceOnPoints')
    instance.inputs['Scale'].default_value = (1,1,1)
    # Connect
    group_links.new(input_node.outputs['Geometry'], distribute.inputs['Mesh'])
    group_links.new(distribute.outputs['Points'], instance.inputs['Points'])
    group_links.new(random_val.outputs['Value'], instance.inputs['Scale'])
    # Need object info for moss plane
    obj_info = group_nodes.new(type='GeometryNodeObjectInfo')
    obj_info.transform_space = 'RELATIVE'
    obj_info.inputs['Object'].default_value = moss_plane
    group_links.new(obj_info.outputs['Geometry'], instance.inputs['Instance'])
    group_links.new(instance.outputs['Instances'], output_node.inputs['Geometry'])

    gn_mod.node_group = node_group

    # Also add moss roof scatter
    gn_mod_roof = roof_obj.modifiers.new(name="MossRoof", type='NODES')
    # Same node group? We can duplicate or reuse; but we need different seed/density
    # For simplicity, reuse the same but with different seed? We'll create a second group with different seed.
    # To save code, we can just adjust modifier's seed by editing node group input seed? But the seed is inside group.
    # We'll create a separate group for roof with lower density.
    roof_gn = bpy.data.node_groups.new(name="MossRoofGN", type='GeometryNodeTree')
    roof_gn.inputs.new('NodeSocketGeometry', 'Geometry')
    roof_gn.outputs.new('NodeSocketGeometry', 'Geometry')
    roof_nodes = roof_gn.nodes
    roof_links = roof_gn.links
    roof_nodes.clear()
    input_node_r = roof_nodes.new(type='NodeGroupInput')
    input_node_r.location = (-200, 0)
    output_node_r = roof_nodes.new(type='NodeGroupOutput')
    output_node_r.location = (400, 0)
    distribute_r = roof_nodes.new(type='GeometryNodeDistributePointsOnFaces')
    distribute_r.distribute_method = 'RANDOM'
    distribute_r.inputs['Density'].default_value = 100
    distribute_r.inputs['Seed'].default_value = house_id * 100 + 50
    random_val_r = roof_nodes.new(type='FunctionNodeRandomValue')
    random_val_r.data_type = 'FLOAT'
    random_val_r.inputs['Min'].default_value = 0.3
    random_val_r.inputs['Max'].default_value = 1.0
    instance_r = roof_nodes.new(type='GeometryNodeInstanceOnPoints')
    obj_info_r = roof_nodes.new(type='GeometryNodeObjectInfo')
    obj_info_r.transform_space = 'RELATIVE'
    obj_info_r.inputs['Object'].default_value = moss_plane
    roof_links.new(input_node_r.outputs['Geometry'], distribute_r.inputs['Mesh'])
    roof_links.new(distribute_r.outputs['Points'], instance_r.inputs['Points'])
    roof_links.new(random_val_r.outputs['Value'], instance_r.inputs['Scale'])
    roof_links.new(obj_info_r.outputs['Geometry'], instance_r.inputs['Instance'])
    roof_links.new(instance_r.outputs['Instances'], output_node_r.inputs['Geometry'])
    gn_mod_roof.node_group = roof_gn

    # Return list of objects for later use? Not needed.
    return

def create_village():
    """Create multiple houses in a village layout."""
    # Ground
    ground = create_ground(size=35, subdivisions=60)
    ground.location.z = -0.1

    # Houses positions around center
    positions = [
        (0, 0, 0),
        (3.5, 2.0, 0),
        (-2.5, 3.5, 0),
        (1.5, -3.0, 0),
        (-3.0, -2.0, 0)
    ]
    scales = [
        (1.0, 1.0, 1.0),
        (0.8, 0.9, 0.8),
        (1.1, 1.0, 0.9),
        (0.7, 0.8, 0.7),
        (0.9, 0.9, 0.8)
    ]
    rotations = [
        (0,0,0),
        (0,0,0.3),
        (0,0,-0.2),
        (0,0,0.5),
        (0,0,-0.4)
    ]

    for i, loc in enumerate(positions):
        create_house(
            location=loc,
            rotation=rotations[i],
            scale=scales[i],
            house_id=i+1
        )

    # Add some scattered debris and grass using geometry nodes on ground
    # Grass geometry nodes on ground
    grass_material = make_grass_material()
    # Create a grass blade instance (plane)
    bpy.ops.mesh.primitive_plane_add(size=0.3, location=(0,0,0))
    grass_plane = bpy.context.object
    grass_plane.name = "GrassBlade"
    grass_plane.data.materials.append(grass_material)
    bpy.context.scene.collection.objects.unlink(grass_plane)  # hide from scene but keep in data
    grass_plane.hide_viewport = True
    grass_plane.hide_render = True

    # Geometry nodes on ground for grass
    gn_grass = ground.modifiers.new(name="GrassScatter", type='NODES')
    grass_gn = bpy.data.node_groups.new(name="GrassGN", type='GeometryNodeTree')
    grass_gn.inputs.new('NodeSocketGeometry', 'Geometry')
    grass_gn.outputs.new('NodeSocketGeometry', 'Geometry')
    g_nodes = grass_gn.nodes
    g_links = grass_gn.links
    g_nodes.clear()
    input_g = g_nodes.new('NodeGroupInput'); input_g.location = (-200,0)
    output_g = g_nodes.new('NodeGroupOutput'); output_g.location = (400,0)
    distribute_g = g_nodes.new('GeometryNodeDistributePointsOnFaces')
    distribute_g.distribute_method = 'RANDOM'
    distribute_g.inputs['Density'].default_value = 0.5  # low density for scattered grass
    distribute_g.inputs['Seed'].default_value = 1
    instance_g = g_nodes.new('GeometryNodeInstanceOnPoints')
    obj_info_g = g_nodes.new('GeometryNodeObjectInfo')
    obj_info_g.transform_space = 'RELATIVE'
    obj_info_g.inputs['Object'].default_value = grass_plane
    random_scale = g_nodes.new('FunctionNodeRandomValue')
    random_scale.data_type = 'FLOAT'
    random_scale.inputs['Min'].default_value = 0.5
    random_scale.inputs['Max'].default_value = 1.5
    random_rot = g_nodes.new('FunctionNodeRandomValue')
    random_rot.data_type = 'FLOAT_VECTOR'
    random_rot.inputs['Min'].default_value = (0,0,0)
    random_rot.inputs['Max'].default_value = (0,0,2*math.pi)
    g_links.new(input_g.outputs['Geometry'], distribute_g.inputs['Mesh'])
    g_links.new(distribute_g.outputs['Points'], instance_g.inputs['Points'])
    g_links.new(random_scale.outputs['Value'], instance_g.inputs['Scale'])
    g_links.new(random_rot.outputs['Value'], instance_g.inputs['Rotation'])
    g_links.new(obj_info_g.outputs['Geometry'], instance_g.inputs['Instance'])
    g_links.new(instance_g.outputs['Instances'], output_g.inputs['Geometry'])
    gn_grass.node_group = grass_gn

    # Add some trees or bushes? Not required but could add. We'll skip for simplicity.

    return

# -------------------------------------------------------------------
# Lighting and camera setup
# -------------------------------------------------------------------

def setup_lighting():
    """Create moody lighting with sun and environment."""
    # Sun
    bpy.ops.object.light_add(type='SUN', location=(10, 5, 15))
    sun = bpy.context.object
    sun.name = "Sun"
    sun.data.angle = 0.05
    sun.data.energy = 5
    sun.rotation_euler = (0.3, 0.2, 0.8)  # angled light

    # Environment (background)
    world = bpy.context.scene.world
    if world is None:
        world = bpy.data.worlds.new("World")
        bpy.context.scene.world = world
    world.use_nodes = True
    nodes = world.node_tree.nodes
    links = world.node_tree.links
    nodes.clear()
    # Sky texture
    sky = nodes.new(type='ShaderNodeTexSky')
    sky.sky_type = 'HOSEK_WILKIE'
    sky.sun_direction = (0.5, 0.3, 0.8)
    sky.sun_elevation = 0.3
    sky.sun_rotation = 0.5
    # Background
    bg = nodes.new(type='ShaderNodeBackground')
    bg.inputs['Strength'].default_value = 0.5
    output = nodes.new(type='ShaderNodeOutputWorld')
    links.new(sky.outputs['Color'], bg.inputs['Color'])
    links.new(bg.outputs['Background'], output.inputs['Surface'])

    # Optional fill light
    bpy.ops.object.light_add(type='AREA', location=(-5, 0, 5))
    fill = bpy.context.object
    fill.name = "FillLight"
    fill.data.energy = 100
    fill.data.size = 5
    fill.rotation_euler = (0.5, 0, 0.8)

def setup_camera():
    """Position camera for a compelling view of the village."""
    bpy.ops.object.camera_add(location=(8, -6, 4))
    cam = bpy.context.object
    cam.name = "Camera"
    cam.rotation_euler = (1.1, 0, 0.9)
    cam.data.lens = 35
    cam.data.sensor_width = 36
    bpy.context.scene.camera = cam

# -------------------------------------------------------------------
# Main execution
# -------------------------------------------------------------------

def main():
    clear_scene()
    create_village()
    setup_lighting()
    setup_camera()

    # Set render settings (benchmark overrides, but set anyway)
    scene = bpy.context.scene
    scene.render.engine = 'CYCLES'
    scene.render.resolution_x = 1536
    scene.render.resolution_y = 1536
    scene.render.film_transparent = False
    scene.view_settings.view_transform = 'Filmic'
    scene.view_settings.look = 'None'
    scene.cycles.samples = 64
    scene.cycles.use_denoising = True

    # Make sure all collections are visible
    for coll in bpy.data.collections:
        coll.hide_viewport = False
        coll.hide_render = False

if __name__ == "__main__":
    main()
```


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

# -------------------------------------------------------------------
# Utility functions
# -------------------------------------------------------------------

def clear_scene():
    """Delete all objects, collections, and data blocks."""
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=False)
    # Also remove any leftover meshes, materials, etc.
    for block in bpy.data.meshes:
        bpy.data.meshes.remove(block)
    for block in bpy.data.materials:
        bpy.data.materials.remove(block)
    for block in bpy.data.textures:
        bpy.data.textures.remove(block)
    for block in bpy.data.images:
        bpy.data.images.remove(block)
    for block in bpy.data.curves:
        bpy.data.curves.remove(block)
    for block in bpy.data.lights:
        bpy.data.lights.remove(block)
    for block in bpy.data.cameras:
        bpy.data.cameras.remove(block)
    collections = list(bpy.data.collections)
    for coll in collections:
        bpy.data.collections.remove(coll)

def new_material(name, use_nodes=True):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = use_nodes
    return mat

def add_texture_coordinate(mat):
    nodes = mat.node_tree.nodes
    tc = nodes.new(type='ShaderNodeTexCoord')
    return tc

def add_mapping(mat):
    nodes = mat.node_tree.nodes
    mapping = nodes.new(type='ShaderNodeMapping')
    return mapping

# -------------------------------------------------------------------
# Material creation
# -------------------------------------------------------------------

def make_wood_material():
    """Weathered wood with grain and color variation."""
    mat = new_material("WeatheredWood")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links

    # Clear default nodes
    for node in nodes:
        nodes.remove(node)

    # Principled BSDF
    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    # Output
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    # Texture coordinates and mapping
    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    mapping.inputs['Scale'].default_value = (0.5, 0.5, 0.5)
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Wood grain wave texture
    wave = nodes.new(type='ShaderNodeTexWave')
    wave.wave_type = 'BANDS'
    wave.inputs['Scale'].default_value = 2.0
    wave.inputs['Distortion'].default_value = 3.0
    links.new(mapping.outputs['Vector'], wave.inputs['Vector'])

    # Color ramp for wood tones
    ramp = nodes.new(type='ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].color = (0.2, 0.1, 0.05, 1)
    ramp.color_ramp.elements[0].position = 0.0
    ramp.color_ramp.elements[1].color = (0.55, 0.35, 0.15, 1)
    ramp.color_ramp.elements[1].position = 1.0
    links.new(wave.outputs['Fac'], ramp.inputs['Fac'])

    # Mix with noise for weathering
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 5.0
    noise.inputs['Detail'].default_value = 5.0
    noise.inputs['Roughness'].default_value = 0.7
    mix = nodes.new(type='ShaderNodeMixRGB')
    mix.blend_type = 'MIX'
    mix.inputs['Fac'].default_value = 0.3
    links.new(ramp.outputs['Color'], mix.inputs['Color1'])
    links.new(noise.outputs['Color'], mix.inputs['Color2'])

    # Bump map
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.3
    links.new(noise.outputs['Fac'], bump.inputs['Height'])

    # Connect to principled
    links.new(mix.outputs['Color'], principled.inputs['Base Color'])
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

def make_metal_rust_material():
    """Rusted corrugated metal roof."""
    mat = new_material("RustedMetal")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    mapping.inputs['Scale'].default_value = (0.8, 0.8, 0.8)
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Base metal color
    base_metal = (0.3, 0.3, 0.35, 1)

    # Rust noise
    noise_rust = nodes.new(type='ShaderNodeTexNoise')
    noise_rust.inputs['Scale'].default_value = 3.0
    noise_rust.inputs['Detail'].default_value = 6.0
    noise_rust.inputs['Roughness'].default_value = 0.5

    mix_rust = nodes.new(type='ShaderNodeMixRGB')
    mix_rust.blend_type = 'MIX'
    # Colors: metal then rust
    color_metal = (0.4, 0.4, 0.45, 1)
    color_rust = (0.6, 0.15, 0.05, 1)
    links.new(noise_rust.outputs['Fac'], mix_rust.inputs['Fac'])
    mix_rust.inputs['Color1'].default_value = color_metal
    mix_rust.inputs['Color2'].default_value = color_rust
    links.new(tc.outputs['Object'], mix_rust.inputs['Vector'])  # not needed

    # Corrugation using wave texture
    wave = nodes.new(type='ShaderNodeTexWave')
    wave.wave_type = 'BANDS'
    wave.inputs['Scale'].default_value = 0.5
    wave.inputs['Distortion'].default_value = 2.0
    # Multiply wave with rust mix for variation
    mult = nodes.new(type='ShaderNodeMixRGB')
    mult.blend_type = 'MULTIPLY'
    links.new(wave.outputs['Fac'], mult.inputs['Fac'])
    links.new(mix_rust.outputs['Color'], mult.inputs['Color1'])
    links.new(mix_rust.outputs['Color'], mult.inputs['Color2'])

    # Roughness map from noise
    noise_rough = nodes.new(type='ShaderNodeTexNoise')
    noise_rough.inputs['Scale'].default_value = 2.0
    roughness_ramp = nodes.new(type='ShaderNodeValToRGB')
    roughness_ramp.color_ramp.elements[0].color = (0.6, 0.6, 0.6, 1)
    roughness_ramp.color_ramp.elements[0].position = 0.0
    roughness_ramp.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1)
    roughness_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise_rough.outputs['Fac'], roughness_ramp.inputs['Fac'])

    # Normal map for dents
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.5
    links.new(noise_rough.outputs['Fac'], bump.inputs['Height'])

    # Connect
    links.new(mult.outputs['Color'], principled.inputs['Base Color'])
    links.new(roughness_ramp.outputs['Color'], principled.inputs['Roughness'])
    principled.inputs['Metallic'].default_value = 0.8
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

def make_moss_material():
    """Moss for ground and walls."""
    mat = new_material("Moss")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    mapping.inputs['Scale'].default_value = (2.0, 2.0, 2.0)
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Base green
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 2.0
    noise.inputs['Detail'].default_value = 8.0
    color_ramp = nodes.new(type='ShaderNodeValToRGB')
    color_ramp.color_ramp.elements[0].color = (0.1, 0.4, 0.1, 1)
    color_ramp.color_ramp.elements[0].position = 0.0
    color_ramp.color_ramp.elements[1].color = (0.3, 0.6, 0.2, 1)
    color_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise.outputs['Fac'], color_ramp.inputs['Fac'])

    # Bump
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.8
    links.new(noise.outputs['Fac'], bump.inputs['Height'])

    links.new(color_ramp.outputs['Color'], principled.inputs['Base Color'])
    principled.inputs['Roughness'].default_value = 0.9
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

def make_grass_material():
    """Grass blade material (transparent planes)."""
    mat = new_material("Grass")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    # Color variation
    tc = nodes.new(type='ShaderNodeTexCoord')
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 3.0
    color_ramp = nodes.new(type='ShaderNodeValToRGB')
    color_ramp.color_ramp.elements[0].color = (0.2, 0.6, 0.1, 1)
    color_ramp.color_ramp.elements[0].position = 0.0
    color_ramp.color_ramp.elements[1].color = (0.4, 0.8, 0.2, 1)
    color_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise.outputs['Fac'], color_ramp.inputs['Fac'])
    links.new(tc.outputs['UV'], noise.inputs['Vector'])

    # Transparency (alpha from noise)
    # We'll use a separate texture for alpha to create blade shape
    # But for simplicity, use a wave texture to create vertical stripes
    wave = nodes.new(type='ShaderNodeTexWave')
    wave.wave_type = 'BANDS'
    wave.inputs['Scale'].default_value = 10.0
    # Map to UV along height
    # Add map range to concentrate alpha at top?
    # Alternatively, use a simple gradient
    gradient = nodes.new(type='ShaderNodeTexGradient')
    gradient.gradient_type = 'LINEAR'
    musgrave = nodes.new(type='ShaderNodeTexMusgrave')
    musgrave.inputs['Scale'].default_value = 5.0
    # Mix for random blade shapes
    mix = nodes.new(type='ShaderNodeMixRGB')
    mix.blend_type = 'MULTIPLY'
    links.new(gradient.outputs['Color'], mix.inputs['Color1'])
    links.new(musgrave.outputs['Fac'], mix.inputs['Color2'])
    links.new(tc.outputs['UV'], gradient.inputs['Vector'])
    links.new(tc.outputs['UV'], musgrave.inputs['Vector'])

    links.new(color_ramp.outputs['Color'], principled.inputs['Base Color'])
    links.new(mix.outputs['Color'], principled.inputs['Alpha'])
    principled.inputs['Roughness'].default_value = 0.7

    # Enable transparency
    mat.blend_method = 'BLEND'
    mat.shadow_method = 'HASHED'

    return mat

def make_vine_material():
    """Vine material with leaves."""
    mat = new_material("Vine")
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    for node in nodes:
        nodes.remove(node)

    principled = nodes.new(type='ShaderNodeBsdfPrincipled')
    principled.location = (0, 0)
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (300, 0)
    links.new(principled.outputs['BSDF'], output.inputs['Surface'])

    # Stem and leaf color mix
    tc = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    links.new(tc.outputs['UV'], mapping.inputs['Vector'])

    # Noise for leaf clusters
    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 2.0
    color_ramp = nodes.new(type='ShaderNodeValToRGB')
    color_ramp.color_ramp.elements[0].color = (0.3, 0.5, 0.1, 1)  # leaf
    color_ramp.color_ramp.elements[0].position = 0.0
    color_ramp.color_ramp.elements[1].color = (0.05, 0.02, 0.01, 1)  # stem
    color_ramp.color_ramp.elements[1].position = 1.0
    links.new(noise.outputs['Fac'], color_ramp.inputs['Fac'])

    # Bump
    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.4
    links.new(noise.outputs['Fac'], bump.inputs['Height'])

    links.new(color_ramp.outputs['Color'], principled.inputs['Base Color'])
    principled.inputs['Roughness'].default_value = 0.8
    links.new(bump.outputs['Normal'], principled.inputs['Normal'])

    return mat

# -------------------------------------------------------------------
# Geometry creation
# -------------------------------------------------------------------

def create_ground(size=30, subdivisions=50):
    """Create a slightly hilly ground plane."""
    bpy.ops.mesh.primitive_grid_add(x_subdivisions=subdivisions, y_subdivisions=subdivisions, size=size, location=(0,0,0))
    ground = bpy.context.object
    ground.name = "Ground"

    # Displace with noise for gentle hills
    displace = ground.modifiers.new(name='Displace', type='DISPLACE')
    tex = bpy.data.textures.new('GroundNoise', type='CLOUDS')
    tex.noise_scale = 2.0
    tex.noise_depth = 2
    displace.texture = tex
    displace.strength = 0.5
    displace.mid_level = 0.0

    # Apply to have actual geometry for scattering later (or keep as modifier)
    # We'll keep it as modifier because we will apply later? Not needed for GN scatter.
    # Add material
    ground_material = new_material("Ground")
    nodes = ground_material.node_tree.nodes
    nodes.clear()
    # Use a simple mix of dirt and grass
    principled = nodes.new('ShaderNodeBsdfPrincipled')
    output = nodes.new('ShaderNodeOutputMaterial')
    nodes.new('ShaderNodeTexCoord')
    nodes.new('ShaderNodeMapping')
    # We'll create a quick node setup for ground
    # For brevity, assign a green-brown color
    principled.inputs['Base Color'].default_value = (0.3, 0.25, 0.15, 1)
    principled.inputs['Roughness'].default_value = 1.0
    ground_material.node_tree.links.new(principled.outputs['BSDF'], output.inputs['Surface'])
    ground.data.materials.append(ground_material)

    return ground

def create_house(location, rotation=(0,0,0), scale=(1,1,1), house_id=1):
    """Create a weathered house with metal roof."""
    # Container collection for this house
    coll = bpy.data.collections.new(f"House_{house_id}")
    bpy.context.scene.collection.children.link(coll)

    # Dimensions
    width = 2.5 * scale[0]
    depth = 2.0 * scale[1]
    height = 1.8 * scale[2]

    # Walls
    bpy.ops.mesh.primitive_cube_add(size=1, location=(0,0,0))
    walls = bpy.context.object
    walls.name = f"Walls_{house_id}"
    walls.scale = (width, depth, height)
    walls.location = (location[0], location[1], location[2] + height/2)
    coll.objects.link(walls)
    bpy.context.collection.objects.unlink(walls)
    walls.data.materials.append(make_wood_material())

    # Roof (prism shape)
    # We'll create a simple triangular roof using two planes angled, or use a cube and cut
    # Simpler: use a cube scaled and rotated
    roof_height = 0.6 * scale[2]
    roof_peak = location[2] + height + roof_height/2
    # Create a pyramid-like shape: use a triangle extruded
    # Use mesh from scratch
    roof_verts = [
        (-width/2, -depth/2, 0),
        (width/2, -depth/2, 0),
        (width/2, depth/2, 0),
        (-width/2, depth/2, 0),
        (0, -depth/2, roof_height),
        (0, depth/2, roof_height)
    ]
    roof_faces = [
        (0, 1, 4),
        (1, 2, 5, 4),
        (2, 3, 5),
        (3, 0, 4, 5)
    ]
    roof_mesh = bpy.data.meshes.new(f"Roof_{house_id}")
    roof_mesh.from_pydata(roof_verts, [], roof_faces)
    roof_mesh.update()
    roof_obj = bpy.data.objects.new(f"Roof_{house_id}", roof_mesh)
    roof_obj.location = (location[0], location[1], location[2] + height)
    coll.objects.link(roof_obj)
    bpy.context.collection.objects.unlink(roof_obj)
    roof_obj.data.materials.append(make_metal_rust_material())

    # Doors and windows - add as simple boolean cuts or separate geometry
    # For simplicity, add broken window frames with glass shards
    # We'll use a few planes with transparency for glass shards
    # Window positions
    windows = []
    # Front window
    windows.append((0, depth/2 * 0.8, height*0.5, 0.4, 0.5))
    # Side windows
    windows.append((width/2 * 0.7, 0, height*0.5, 0.4, 0.5))
    windows.append((-width/2 * 0.7, 0, height*0.5, 0.4, 0.5))
    # Back window
    windows.append((0, -depth/2 * 0.8, height*0.5, 0.4, 0.5))

    for (wx, wy, wz, ww, wh) in windows:
        # Create a flat plane for window opening (cutout) or just glass shards
        # We'll add broken glass shards (small triangles)
        shard_material = new_material(f"GlassShard_{house_id}_{wx}")
        shard_material.use_nodes = True
        nodes = shard_material.node_tree.nodes
        for n in nodes:
            nodes.remove(n)
        glass = nodes.new('ShaderNodeBsdfGlass')
        glass.inputs['Color'].default_value = (0.8, 0.9, 1.0, 1)
        glass.inputs['Roughness'].default_value = 0.2
        output = nodes.new('ShaderNodeOutputMaterial')
        shard_material.node_tree.links.new(glass.outputs['BSDF'], output.inputs['Surface'])

        # Create a few shard planes
        for _ in range(random.randint(3,6)):
            sx = wx + random.uniform(-0.15, 0.15)
            sy = wy + random.uniform(-0.15, 0.15)
            sz = wz + random.uniform(-0.15, 0.15)
            bpy.ops.mesh.primitive_plane_add(size=random.uniform(0.05, 0.15), location=(location[0]+sx, location[1]+sy, location[2]+sz))
            shard = bpy.context.object
            shard.name = f"Shard_{house_id}"
            coll.objects.link(shard)
            bpy.context.collection.objects.unlink(shard)
            shard.data.materials.append(shard_material)
            # Random rotation
            shard.rotation_euler = (random.random()*2*math.pi, random.random()*2*math.pi, random.random()*2*math.pi)

    # Add some vines climbing the walls using curves
    # We'll create a curve with a vine material and use geometry nodes to distribute leaves?
    # For simplicity, we'll create a few curve paths with circular bevel.
    vine_material = make_vine_material()
    for side in range(4):
        if side == 0: # front
            start = (location[0], location[1] + depth/2, location[2])
            end = (location[0], location[1] + depth/2, location[2] + height*0.8)
        elif side == 1: # right
            start = (location[0] + width/2, location[1], location[2])
            end = (location[0] + width/2, location[1], location[2] + height*0.6)
        elif side == 2: # back
            start = (location[0], location[1] - depth/2, location[2])
            end = (location[0], location[1] - depth/2, location[2] + height*0.7)
        else: # left
            start = (location[0] - width/2, location[1], location[2])
            end = (location[0] - width/2, location[1], location[2] + height*0.5)
        # Create curve
        curve_data = bpy.data.curves.new(f"VineCurve_{house_id}_{side}", 'CURVE')
        curve_data.dimensions = '3D'
        curve_data.fill_mode = 'FULL'
        curve_data.bevel_depth = 0.04
        curve_data.bevel_resolution = 4
        spline = curve_data.splines.new('BEZIER')
        spline.bezier_points.add(1)
        spline.bezier_points[0].co = start
        spline.bezier_points[1].co = end
        # Add some control points for wavy shape
        # We'll just use two points for simplicity
        curve_obj = bpy.data.objects.new(f"Vine_{house_id}_{side}", curve_data)
        coll.objects.link(curve_obj)
        bpy.context.collection.objects.unlink(curve_obj)
        curve_obj.data.materials.append(vine_material)

    # Add moss patches on walls and roof using geometry nodes
    # We'll create a geometry node group that scatters moss on faces
    # Alternatively, we can use a particle system, but geometry nodes are simpler to script
    # We'll apply a geometry nodes modifier to the house walls (and roof) that instances moss planes on faces
    # For the roof, we'll also add rusted patches via material already, but moss is physical.

    # Create a moss instance object: a small plane or cube with moss material
    bpy.ops.mesh.primitive_plane_add(size=0.1, location=(0,0,0))
    moss_plane = bpy.context.object
    moss_plane.name = f"MossPlane_{house_id}"
    moss_plane.data.materials.append(make_moss_material())
    coll.objects.link(moss_plane)
    bpy.context.collection.objects.unlink(moss_plane)
    moss_plane.hide_viewport = True
    moss_plane.hide_render = True  # hide original, only used as instance

    # Geometry nodes on walls
    gn_mod = walls.modifiers.new(name="MossScatter", type='NODES')
    # Create geometry node group
    node_group = bpy.data.node_groups.new(name="MossScatterGN", type='GeometryNodeTree')
    node_group.inputs.new('NodeSocketGeometry', 'Geometry')
    node_group.outputs.new('NodeSocketGeometry', 'Geometry')

    group_nodes = node_group.nodes
    group_links = node_group.links
    group_nodes.clear()

    # Input node
    input_node = group_nodes.new(type='NodeGroupInput')
    input_node.location = (-200, 0)
    # Output node
    output_node = group_nodes.new(type='NodeGroupOutput')
    output_node.location = (400, 0)

    # Distribute points on faces
    distribute = group_nodes.new(type='GeometryNodeDistributePointsOnFaces')
    distribute.distribute_method = 'RANDOM'
    distribute.inputs['Density'].default_value = 200
    distribute.inputs['Seed'].default_value = house_id * 100 + side
    # Add scale random
    random_val = group_nodes.new(type='FunctionNodeRandomValue')
    random_val.data_type = 'FLOAT'
    random_val.inputs['Min'].default_value = 0.5
    random_val.inputs['Max'].default_value = 1.5

    # Instance on points
    instance = group_nodes.new(type='GeometryNodeInstanceOnPoints')
    instance.inputs['Scale'].default_value = (1,1,1)
    # Connect
    group_links.new(input_node.outputs['Geometry'], distribute.inputs['Mesh'])
    group_links.new(distribute.outputs['Points'], instance.inputs['Points'])
    group_links.new(random_val.outputs['Value'], instance.inputs['Scale'])
    # Need object info for moss plane
    obj_info = group_nodes.new(type='GeometryNodeObjectInfo')
    obj_info.transform_space = 'RELATIVE'
    obj_info.inputs['Object'].default_value = moss_plane
    group_links.new(obj_info.outputs['Geometry'], instance.inputs['Instance'])
    group_links.new(instance.outputs['Instances'], output_node.inputs['Geometry'])

    gn_mod.node_group = node_group

    # Also add moss roof scatter
    gn_mod_roof = roof_obj.modifiers.new(name="MossRoof", type='NODES')
    # Same node group? We can duplicate or reuse; but we need different seed/density
    # For simplicity, reuse the same but with different seed? We'll create a second group with different seed.
    # To save code, we can just adjust modifier's seed by editing node group input seed? But the seed is inside group.
    # We'll create a separate group for roof with lower density.
    roof_gn = bpy.data.node_groups.new(name="MossRoofGN", type='GeometryNodeTree')
    roof_gn.inputs.new('NodeSocketGeometry', 'Geometry')
    roof_gn.outputs.new('NodeSocketGeometry', 'Geometry')
    roof_nodes = roof_gn.nodes
    roof_links = roof_gn.links
    roof_nodes.clear()
    input_node_r = roof_nodes.new(type='NodeGroupInput')
    input_node_r.location = (-200, 0)
    output_node_r = roof_nodes.new(type='NodeGroupOutput')
    output_node_r.location = (400, 0)
    distribute_r = roof_nodes.new(type='GeometryNodeDistributePointsOnFaces')
    distribute_r.distribute_method = 'RANDOM'
    distribute_r.inputs['Density'].default_value = 100
    distribute_r.inputs['Seed'].default_value = house_id * 100 + 50
    random_val_r = roof_nodes.new(type='FunctionNodeRandomValue')
    random_val_r.data_type = 'FLOAT'
    random_val_r.inputs['Min'].default_value = 0.3
    random_val_r.inputs['Max'].default_value = 1.0
    instance_r = roof_nodes.new(type='GeometryNodeInstanceOnPoints')
    obj_info_r = roof_nodes.new(type='GeometryNodeObjectInfo')
    obj_info_r.transform_space = 'RELATIVE'
    obj_info_r.inputs['Object'].default_value = moss_plane
    roof_links.new(input_node_r.outputs['Geometry'], distribute_r.inputs['Mesh'])
    roof_links.new(distribute_r.outputs['Points'], instance_r.inputs['Points'])
    roof_links.new(random_val_r.outputs['Value'], instance_r.inputs['Scale'])
    roof_links.new(obj_info_r.outputs['Geometry'], instance_r.inputs['Instance'])
    roof_links.new(instance_r.outputs['Instances'], output_node_r.inputs['Geometry'])
    gn_mod_roof.node_group = roof_gn

    # Return list of objects for later use? Not needed.
    return

def create_village():
    """Create multiple houses in a village layout."""
    # Ground
    ground = create_ground(size=35, subdivisions=60)
    ground.location.z = -0.1

    # Houses positions around center
    positions = [
        (0, 0, 0),
        (3.5, 2.0, 0),
        (-2.5, 3.5, 0),
        (1.5, -3.0, 0),
        (-3.0, -2.0, 0)
    ]
    scales = [
        (1.0, 1.0, 1.0),
        (0.8, 0.9, 0.8),
        (1.1, 1.0, 0.9),
        (0.7, 0.8, 0.7),
        (0.9, 0.9, 0.8)
    ]
    rotations = [
        (0,0,0),
        (0,0,0.3),
        (0,0,-0.2),
        (0,0,0.5),
        (0,0,-0.4)
    ]

    for i, loc in enumerate(positions):
        create_house(
            location=loc,
            rotation=rotations[i],
            scale=scales[i],
            house_id=i+1
        )

    # Add some scattered debris and grass using geometry nodes on ground
    # Grass geometry nodes on ground
    grass_material = make_grass_material()
    # Create a grass blade instance (plane)
    bpy.ops.mesh.primitive_plane_add(size=0.3, location=(0,0,0))
    grass_plane = bpy.context.object
    grass_plane.name = "GrassBlade"
    grass_plane.data.materials.append(grass_material)
    bpy.context.scene.collection.objects.unlink(grass_plane)  # hide from scene but keep in data
    grass_plane.hide_viewport = True
    grass_plane.hide_render = True

    # Geometry nodes on ground for grass
    gn_grass = ground.modifiers.new(name="GrassScatter", type='NODES')
    grass_gn = bpy.data.node_groups.new(name="GrassGN", type='GeometryNodeTree')
    grass_gn.inputs.new('NodeSocketGeometry', 'Geometry')
    grass_gn.outputs.new('NodeSocketGeometry', 'Geometry')
    g_nodes = grass_gn.nodes
    g_links = grass_gn.links
    g_nodes.clear()
    input_g = g_nodes.new('NodeGroupInput'); input_g.location = (-200,0)
    output_g = g_nodes.new('NodeGroupOutput'); output_g.location = (400,0)
    distribute_g = g_nodes.new('GeometryNodeDistributePointsOnFaces')
    distribute_g.distribute_method = 'RANDOM'
    distribute_g.inputs['Density'].default_value = 0.5  # low density for scattered grass
    distribute_g.inputs['Seed'].default_value = 1
    instance_g = g_nodes.new('GeometryNodeInstanceOnPoints')
    obj_info_g = g_nodes.new('GeometryNodeObjectInfo')
    obj_info_g.transform_space = 'RELATIVE'
    obj_info_g.inputs['Object'].default_value = grass_plane
    random_scale = g_nodes.new('FunctionNodeRandomValue')
    random_scale.data_type = 'FLOAT'
    random_scale.inputs['Min'].default_value = 0.5
    random_scale.inputs['Max'].default_value = 1.5
    random_rot = g_nodes.new('FunctionNodeRandomValue')
    random_rot.data_type = 'FLOAT_VECTOR'
    random_rot.inputs['Min'].default_value = (0,0,0)
    random_rot.inputs['Max'].default_value = (0,0,2*math.pi)
    g_links.new(input_g.outputs['Geometry'], distribute_g.inputs['Mesh'])
    g_links.new(distribute_g.outputs['Points'], instance_g.inputs['Points'])
    g_links.new(random_scale.outputs['Value'], instance_g.inputs['Scale'])
    g_links.new(random_rot.outputs['Value'], instance_g.inputs['Rotation'])
    g_links.new(obj_info_g.outputs['Geometry'], instance_g.inputs['Instance'])
    g_links.new(instance_g.outputs['Instances'], output_g.inputs['Geometry'])
    gn_grass.node_group = grass_gn

    # Add some trees or bushes? Not required but could add. We'll skip for simplicity.

    return

# -------------------------------------------------------------------
# Lighting and camera setup
# -------------------------------------------------------------------

def setup_lighting():
    """Create moody lighting with sun and environment."""
    # Sun
    bpy.ops.object.light_add(type='SUN', location=(10, 5, 15))
    sun = bpy.context.object
    sun.name = "Sun"
    sun.data.angle = 0.05
    sun.data.energy = 5
    sun.rotation_euler = (0.3, 0.2, 0.8)  # angled light

    # Environment (background)
    world = bpy.context.scene.world
    if world is None:
        world = bpy.data.worlds.new("World")
        bpy.context.scene.world = world
    world.use_nodes = True
    nodes = world.node_tree.nodes
    links = world.node_tree.links
    nodes.clear()
    # Sky texture
    sky = nodes.new(type='ShaderNodeTexSky')
    sky.sky_type = 'HOSEK_WILKIE'
    sky.sun_direction = (0.5, 0.3, 0.8)
    sky.sun_elevation = 0.3
    sky.sun_rotation = 0.5
    # Background
    bg = nodes.new(type='ShaderNodeBackground')
    bg.inputs['Strength'].default_value = 0.5
    output = nodes.new(type='ShaderNodeOutputWorld')
    links.new(sky.outputs['Color'], bg.inputs['Color'])
    links.new(bg.outputs['Background'], output.inputs['Surface'])

    # Optional fill light
    bpy.ops.object.light_add(type='AREA', location=(-5, 0, 5))
    fill = bpy.context.object
    fill.name = "FillLight"
    fill.data.energy = 100
    fill.data.size = 5
    fill.rotation_euler = (0.5, 0, 0.8)

def setup_camera():
    """Position camera for a compelling view of the village."""
    bpy.ops.object.camera_add(location=(8, -6, 4))
    cam = bpy.context.object
    cam.name = "Camera"
    cam.rotation_euler = (1.1, 0, 0.9)
    cam.data.lens = 35
    cam.data.sensor_width = 36
    bpy.context.scene.camera = cam

# -------------------------------------------------------------------
# Main execution
# -------------------------------------------------------------------

def main():
    clear_scene()
    create_village()
    setup_lighting()
    setup_camera()

    # Set render settings (benchmark overrides, but set anyway)
    scene = bpy.context.scene
    scene.render.engine = 'CYCLES'
    scene.render.resolution_x = 1536
    scene.render.resolution_y = 1536
    scene.render.film_transparent = False
    scene.view_settings.view_transform = 'Filmic'
    scene.view_settings.look = 'None'
    scene.cycles.samples = 64
    scene.cycles.use_denoising = True

    # Make sure all collections are visible
    for coll in bpy.data.collections:
        coll.hide_viewport = False
        coll.hide_render = False

if __name__ == "__main__":
    main()
```