 rely on EEVEE-only effects for the final appearance. All materials must show correctly in Cycles. At this stage we are using Blender workbench, so materials don't render their colors. But we can see the geometry looks amazing!
Wait! The viewport camera hasn't changed. Why?
Ah, the viewport camera stayed in its default position. Let's make sure the camera is active, or we can see the ivy that got added. It is the ladder-like structure climbing up the front left of the house frame! Let's rotate the viewport view or move the camera to frame it closer, or better yet, let's write a python script to set up a nice camera and view it in Cycles or Material Preview mode!

Wait, let's design the layout of the entire village!
Let's see: we want three houses.
Let's place them:
- **House 1**: Main foreground cottage.
  - Position: `Vector((-1.0, -1.5, get_terrain_height(-1.0, -1.5)))`. (This is close to the camera).
  - Rotation around Z: -0.2 (approx -11 degrees).
  - Dimensions: L=5.5m, W=3.8m, H=2.4m, Peak=3.5m.
  - This has multiple Ivy growths: Front side, back side, left side.
- **House 2**: Middle-ground cottage.
  - Position: `Vector((5.5, 6.0, get_terrain_height(5.5, 6.0)))`.
  - Rotation around Z: 1.0 (approx 57 degrees).
  - Dimensions: L=6.5m, W=4.2m, H=2.5m, Peak=3.7m.
  - It also has climbing ivy and is partially collapsed (maybe we can increase the missing roof sheet percentage for it to 35%).
- **House 3**: Background partially buried ruin / barn.
  - Position: `Vector((-8.0, 4.0, get_terrain_height(-8.0, 4.0)))`.
  - Rotation around Z: -0.8 (approx -45 degrees).
  - Dimensions: L=5.0m, W=3.5m, H=2.2m, Peak=3.2m.
  - This one represents a completely collapsed frame. We can make its roof sheets 70% missing, which will look exceptionally dilapidated!

Let's also add **grass and foliage scattered on the ground**!
How can we generate scattered grass / plants on the ground?
We can iterate over our terrain:
- In a grid of size e.g. -20m to 20m, we can randomly place grass clumps.
- A grass clump can be a series of simple planes (2-4 intersecting planes with a grass blade shape or simple tapered shape), with the `Foliage` material.
- We can randomly scale them (e.g. 0.15m to 0.4m tall) and rotate them randomly around the Z-axis.
- This creates an extremely organic, lush, overgrown meadow floor!
- Let's do this! If we write a python loop to generate these grass clumps, we can generate say 200 scatter locations. To make it extremely efficient, we can build the vertex and face arrays for all 200 grass clumps and compile them into a *single mesh* object called `Grass_Scatter`!
  This is a master-class in Blender programming: compiling all grass clumps into a single mesh block. It means 0 viewport lag, instant Cycles loading, and superb details!
Let's design the grass clump mesh creator.
A single grass clump consists of say 3 intersecting vertical planes, each slightly curved or flared outward.
Let's define a simple clump of vertices and faces, scale and rotate it, and add it at various ground locations!
This is absolutely brilliant. Let's do the math:
For a single grass clump centered at (0,0,0):
- It has 3 blades.
- Blade 1: Plane along Y, X=0, Z goes from 0 to height. Peak is at Y=0, Z=height. Let's make it look like a diamond.
- Let's write the code for it! It's super simple.

Let's make sure the grass is dense in the front, and sparser in other places, or evenly distributed. 400 grass clumps compiled into a single mesh will look incredibly lush and dense, especially when combined with our detailed ground moss texture, and the ivy!

Let's write a complete script that:
1. Re-creates the terrain.
2. Generates the 3 houses with their specific sizes, positions, and dilapidation percentages.
3. Generates the Ivy climbing up each house.
4. Generates the scattered Grass clumps across the terrain.
5. Sets up excellent lighting:
   - A warm sunset sun light to hit the metallic roof edges and grass blades, producing beautiful glints.
   - A soft blue/grey sky/ambient light (World shader) representing a quiet afternoon or post-rain overcast glow.
6. Sets up the Camera to frame the village beautifully.
   - We will place the camera around (X=-6.5, Y=-10.0, Z=4.0), aiming towards the main houses (around X=1.0, Y=1.5, Z=1.0).
   - This creates a wonderful composition, with House 1 in the foreground, House 2 in the middle-ground, and the rolling valley behind.

Let's write this script and run it! Let's draft it. This is going to be the master script to build the entire environment. Let's construct the code very cleanly.

import bpy
import math
import random
from mathutils import Vector, Euler, Matrix

# Let's clear ALL mesh/empty objects first to start fresh and clean
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

def get_terrain_height(x, y):
    # Large hills
    z = 1.0 * math.sin(x * 0.1) * math.cos(y * 0.1)
    # Small ridges
    z += 0.4 * math.sin(x * 0.3) * math.sin(y * 0.4)
    # Random undulating micro-details
    z += 0.12 * math.cos(x * 0.8 + y * 0.5)

    # Path/depression
    path_dist = abs(x + y * 0.5) / 10.0
    if path_dist < 1.0:
        z -= (1.0 - path_dist) * 0.6
    return z

def build_materials_if_needed():
    # Make sure materials exist
    for m in ["Rusted_Metal", "Weathered_Wood", "Ground_Moss", "Foliage"]:
        if m in bpy.data.materials:
            pass # Keep them, they are already structured perfectly!

build_materials_if_needed()

# --- 1. RECREATE TERRAIN ---
bpy.ops.mesh.primitive_grid_add(x_subdivisions=100, y_subdivisions=100, size=50, location=(0, 0, 0))
terrain = bpy.context.object
terrain.name = "Terrain"
mat_g = bpy.data.materials.get("Ground_Moss")
if mat_g:
    terrain.data.materials.append(mat_g)

mesh = terrain.data
for vert in mesh.vertices:
    vert.co.z = get_terrain_height(vert.co.x, vert.co.y)
bpy.ops.object.shade_smooth()

# --- GENERATION FUNCTIONS ---

def create_corrugated_sheet_data(w=1.0, l=1.8, wave_h=0.025, wave_num=6):
    # Return vertices and faces for a sheet relative to origin (local coords)
    verts = []
    faces = []
    steps = wave_num * 4
    for j in range(2):
        y = -l/2.0 if j == 0 else l/2.0
        for i in range(steps + 1):
            t = i / steps
            x = -w/2.0 + t * w
            z = wave_h * math.sin(t * wave_num * 2.0 * math.pi)
            verts.append(Vector((x, y, z)))
    for i in range(steps):
        v1 = i
        v2 = i + 1
        v3 = v2 + (steps + 1)
        v4 = v1 + (steps + 1)
        faces.append((v1, v4, v3, v2))
    return verts, faces

def create_house_mesh(name, l=6.0, w=4.0, h=2.5, ph=3.6, missing_roof_prob=0.2, pos=Vector((0,0,0)), rot_z=0.0):
    all_verts = []
    all_faces = []

    # Material slot trackers
    # We will make 1 multi-material object!
    # Material indices: 0 = Wood, 1 = Metal
    materials_list = [bpy.data.materials.get("Weathered_Wood"), bpy.data.materials.get("Rusted_Metal")]
    all_face_mats = []

    p_w = 0.22
    p_d = 0.025

    # Transformation matrix for the house in world space
    house_mat = Matrix.Translation(pos) @ Euler((0, 0, rot_z)).to_matrix().to_4x4()

    def add_box(local_pos, size, rot, mat_idx=0):
        v_offset = len(all_verts)
        sx, sy, sz = size[0]/2.0, size[1]/2.0, size[2]/2.0
        local_verts = [
            Vector((-sx, -sy, -sz)), Vector((sx, -sy, -sz)),
            Vector((sx, sy, -sz)), Vector((-sx, sy, -sz)),
            Vector((-sx, -sy, sz)), Vector((sx, -sy, sz)),
            Vector((sx, sy, sz)), Vector((-sx, sy, sz))
        ]
        local_mat = Matrix.Translation(local_pos) @ rot.to_matrix().to_4x4()
        # Transform local box verts into house space, then into world space
        for lv in local_verts:
            world_v = house_mat @ (local_mat @ lv)
            all_verts.append(world_v)

        local_faces = [
            (0, 1, 2, 3), (4, 7, 6, 5), (0, 4, 5, 1),
            (1, 5, 6, 2), (2, 6, 7, 3), (3, 7, 4, 0)
        ]
        for lf in local_faces:
            all_faces.append((lf[0]+v_offset, lf[1]+v_offset, lf[2]+v_offset, lf[3]+v_offset))
            all_face_mats.append(mat_idx)

    # 1. Timber posts
    post_sz = 0.12
    for cx in [-l/2.0 + post_sz/2.0, l/2.0 - post_sz/2.0]:
        for cy in [-w/2.0 + post_sz/2.0, w/2.0 - post_sz/2.0]:
            add_box(Vector((cx, cy, h/2.0)), (post_sz, post_sz, h), Euler((random.uniform(-0.02, 0.02), random.uniform(-0.02, 0.02), 0)), 0)

    # 2. Support rafters
    beam_sz = 0.08
    num_rafters = 5
    slope_angle = math.atan((ph - h) / (w / 2.0))
    slope_len = math.sqrt((w/2.0)**2 + (ph - h)**2)
    for r in range(num_rafters):
        t = r / (num_rafters - 1)
        rx = -l/2.0 + t * l
        add_box(Vector((rx, 0, h)), (beam_sz, w * 0.9, beam_sz), Euler((0,0,0)), 0)
        # Left rafter
        add_box(Vector((rx, -w/4.0, h + (ph-h)/2.0)), (beam_sz, beam_sz, slope_len), Euler((slope_angle, 0, 0)), 0)
        # Right rafter
        add_box(Vector((rx, w/4.0, h + (ph-h)/2.0)), (beam_sz, beam_sz, slope_len), Euler((-slope_angle, 0, 0)), 0)

    # 3. Front Wall (Y = -w/2)
    num_steps_x = int(l / p_w)
    for i in range(num_steps_x + 1):
        x = -l/2.0 + i * p_w
        if abs(x) < 0.6: # door-way
            continue
        if random.random() < 0.12:
            continue
        plank_h = h
        tilted_angle = 0.0
        pos_offset_z = 0.0
        if random.random() < 0.15:
            plank_h = h * random.uniform(0.12, 0.6)
            pos_offset_z = - (h - plank_h) / 2.0
        if random.random() < 0.25:
            tilted_angle = random.uniform(-0.08, 0.08)
        add_box(Vector((x, -w/2.0, h/2.0 + pos_offset_z)), (p_w * 0.95, p_d, plank_h), Euler((0.0, tilted_angle, 0.0)), 0)

    # 4. Back Wall (Y = w/2)
    for i in range(num_steps_x + 1):
        x = -l/2.0 + i * p_w
        if abs(x) < 0.6 and random.random() < 0.5:
            continue
        if random.random() < 0.12:
            continue
        plank_h = h
        tilted_angle = 0.0
        pos_offset_z = 0.0
        if random.random() < 0.15:
            plank_h = h * random.uniform(0.12, 0.6)
            pos_offset_z = - (h - plank_h) / 2.0
        if random.random() < 0.25:
            tilted_angle = random.uniform(-0.08, 0.08)
        add_box(Vector((x, w/2.0, h/2.0 + pos_offset_z)), (p_w * 0.95, p_d, plank_h), Euler((0.0, tilted_angle, 0.0)), 0)

    # 5. Left Wall (X = -l/2)
    num_steps_y = int(w / p_w)
    for i in range(num_steps_y + 1):
        y = -w/2.0 + i * p_w
        if abs(y) < 0.5: # window
            continue
        if random.random() < 0.12:
            continue
        plank_h = h
        tilted_angle = 0.0
        pos_offset_z = 0.0
        if random.random() < 0.15:
            plank_h = h * random.uniform(0.12, 0.6)
            pos_offset_z = - (h - plank_h) / 2.0
        if random.random() < 0.25:
            tilted_angle = random.uniform(-0.08, 0.08)
        add_box(Vector((-l/2.0, y, h/2.0 + pos_offset_z)), (p_w * 0.95, p_d, plank_h), Euler((tilted_angle, 0.0, math.pi/2.0)), 0)

    # 6. Right Wall (X = l/2)
    for i in range(num_steps_y + 1):
        y = -w/2.0 + i * p_w
        if abs(y) < 0.5 and random.random() < 0.5:
            continue
        if random.random() < 0.12:
            continue
        plank_h = h
        tilted_angle = 0.0
        pos_offset_z = 0.0
        if random.random() < 0.15:
            plank_h = h * random.uniform(0.12, 0.6)
            pos_offset_z = - (h - plank_h) / 2.0
        if random.random() < 0.25:
            tilted_angle = random.uniform(-0.08, 0.08)
        add_box(Vector((l/2.0, y, h/2.0 + pos_offset_z)), (p_w * 0.95, p_d, plank_h), Euler((tilted_angle, 0.0, math.pi/2.0)), 0)

    # 7. Gable walls
    p_h = 0.20
    g_h = ph - h
    for j in range(int(g_h / p_h) + 1):
        rel_h = j * p_h
        cl_rel_h = min(rel_h, g_h)
        plank_w = (1.0 - (cl_rel_h / g_h)) * w
        if plank_w < 0.1: continue
        # Left gable
        if random.random() > 0.15:
            add_box(Vector((-l/2.0, 0, h + cl_rel_h + p_h/2.0)), (plank_w * 0.98, p_d, p_h * 0.95), Euler((0, 0, math.pi/2.0)), 0)
        # Right gable
        if random.random() > 0.15:
            add_box(Vector((l/2.0, 0, h + cl_rel_h + p_h/2.0)), (plank_w * 0.98, p_d, p_h * 0.95), Euler((0, 0, math.pi/2.0)), 0)

    # 8. Corrugated sheets (Metal)
    sheet_w = 0.95
    sheet_l = 1.35
    slope_angle = math.atan((ph - h) / (w / 2.0))
    slope_len = math.sqrt((w/2.0)**2 + (ph - h)**2) + 0.35

    for slope_side in [-1, 1]:
        rot_x = slope_side * slope_angle
        cols = int((l + 0.4) / (sheet_w * 0.85)) + 1
        rows = 2

        for r in range(rows):
            for c in range(cols):
                if random.random() < missing_roof_prob:
                    continue

                t_slope = 0.25 if r == 0 else 0.75
                dist_slope = t_slope * slope_len - 0.15

                ly = slope_side * (dist_slope * math.cos(slope_angle))
                lz = h + dist_slope * math.sin(slope_angle)
                lx = -l/2.0 - 0.1 + c * (sheet_w * 0.82)

                offset_x = random.uniform(-0.02, 0.02)
                offset_y = random.uniform(-0.02, 0.02)
                offset_z = random.uniform(-0.01, 0.01)

                rot_offset_x = 0.0
                rot_offset_y = 0.0
                rot_offset_z = 0.0

                if random.random() < 0.2: # Slipped sheet
                    offset_y += slope_side * random.uniform(0.12, 0.45)
                    offset_z -= random.uniform(0.04, 0.08)
                    rot_offset_z = random.uniform(-0.18, 0.18)
                    rot_offset_x = random.uniform(-0.04, 0.04)
                elif random.random() < 0.15:
                    rot_offset_z = random.uniform(-0.05, 0.05)

                # Get vertices & faces for a single sheet
                s_verts, s_faces = create_corrugated_sheet_data(w=sheet_w, l=sheet_l)

                # Transform sheet verts into World Space
                sheet_local_pos = Vector((lx + offset_x, ly + offset_y, lz + offset_z))
                sheet_local_rot = Euler((rot_x + rot_offset_x, rot_offset_y, rot_offset_z))
                sheet_local_matrix = Matrix.Translation(sheet_local_pos) @ sheet_local_rot.to_matrix().to_4x4()

                v_offset = len(all_verts)
                for sv in s_verts:
                    # Convert to house space then to world space
                    world_sv = house_mat @ (sheet_local_matrix @ sv)
                    all_verts.append(world_sv)

                for sf in s_faces:
                    all_faces.append((sf[0]+v_offset, sf[1]+v_offset, sf[2]+v_offset, sf[3]+v_offset))
                    all_face_mats.append(1) # Material index 1 (Metal)

    # Compile the final integrated mesh of the house!
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.collection.objects.link(obj)

    v_tuples = [v.to_tuple() for v in all_verts]
    mesh.from_pydata(v_tuples, [], all_faces)
    mesh.update()

    # Assign materials
    for mat in materials_list:
        if mat:
            obj.data.materials.append(mat)

    # Assign material indices to faces
    for face_idx, mat_idx in enumerate(all_face_mats):
        mesh.polygons[face_idx].material_index = mat_idx

    # Set Solidify modifier ONLY to the metal sheets
    # To do this cleanly, let's add a Solidify modifier over the whole mesh.
    # Solidify will make all planks slightly thicker too, which is actually very nice!
    solidify = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
    solidify.thickness = 0.012

    # Smooth shade
    bpy.ops.object.select_all(action='DESELECT')
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.shade_smooth()

    return obj

# Test create house mesh
h1_pos = Vector((-1.0, -1.5, get_terrain_height(-1.0, -1.5)))
create_house_mesh("House_H1", l=5.5, w=3.8, h=2.4, ph=3.5, missing_roof_prob=0.22, pos=h1_pos, rot_z=-0.2)

print("H1 mesh compiled and verified!")

Compile fully integrated multi-material cottage generator combining wood walls and rusted metal sheets


Code executed successfully: H1 mesh compiled and verified!