thought
Wow! Combining Wood (material index 0) and Metal (material index 1) into a single unified multi-material object is an incredibly smart, clean, and highly robust way of designing the cottages in Blender. It completely eliminates joining operations and works flawlessly.

Now, let's write a python function to generate the climbing ivy for these houses.
Wait! Since the ivy climbs in local house coordinates, we can transform the generated ivy mesh points using the SAME house transformation matrix!
This is extremely simple and clean!
Let's see: `generate_house_ivy_mesh` builds the vertices in local coordinates.
Then we can transform all of its vertices into world space by multiplying them by `house_mat`!
Let's modify `generate_house_ivy_mesh` to:
- Take the `house_mat` (which we define using the house position and Z-rotation).
- Transform all of its vertices into world space before compiling the mesh.
- This means the Ivy will align perfectly with the house, and we don't have to rotate/translate the object afterward; it will be built in world space exactly where the house is!
This is incredibly elegant. Let's do that! Let's write the modified Ivy function and test it. Let's define it.

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

def generate_house_ivy_mesh(name, l=6.0, w=4.0, h=2.5, ph=3.6, side='front', x_start=1.0, num_branches=3, pos=Vector((0,0,0)), rot_z=0.0):
    all_verts = []
    all_faces = []

    stem_r = 0.012
    leaf_sz = 0.15

    # World matrix of the house
    house_mat = Matrix.Translation(pos) @ Euler((0, 0, rot_z)).to_matrix().to_4x4()

    # Helper to add a stem segment (cylinder-like box)
    def add_stem_segment(p1, p2):
        v_offset = len(all_verts)
        dir_v = p2 - p1
        dist = dir_v.length
        if dist < 0.001:
            return

        center = p1 + dir_v * 0.5
        rotation = dir_v.to_track_quat('Z', 'Y').to_euler()

        sx, sy, sz = stem_r, stem_r, dist/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(center) @ rotation.to_matrix().to_4x4()
        # Transform local vertex into world space via house matrix
        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))

    # Helper to add a leaf
    def add_leaf(pos_local, normal_local):
        v_offset = len(all_verts)
        sz = leaf_sz / 2.0
        rot_angle = random.uniform(0.0, 2.0 * math.pi)

        up = Vector((0, 0, 1))
        tangent = normal_local.cross(up)
        if tangent.length < 0.01:
            tangent = normal_local.cross(Vector((0, 1, 0)))
        tangent.normalize()
        bitangent = normal_local.cross(tangent).normalized()

        # Local leaf vertices
        l_verts = [
            pos_local - tangent * sz - bitangent * sz + normal_local * 0.02,
            pos_local + tangent * sz - bitangent * sz + normal_local * 0.02,
            pos_local + tangent * sz + bitangent * sz + normal_local * 0.08,
            pos_local - tangent * sz + bitangent * sz + normal_local * 0.08
        ]

        # Transform into World Space
        for lv in l_verts:
            all_verts.append(house_mat @ lv)

        all_faces.append((v_offset, v_offset+1, v_offset+2, v_offset+3))

    # Generate each branch
    for b in range(num_branches):
        curr_x = x_start + random.uniform(-0.15, 0.15)
        curr_z = -0.1 # slightly below ground

        if side == 'front':
            curr_y = -w/2.0 - 0.02
            wall_normal = Vector((0, -1, 0))
        elif side == 'back':
            curr_y = w/2.0 + 0.02
            wall_normal = Vector((0, 1, 0))
        elif side == 'left':
            curr_x = -l/2.0 - 0.02
            curr_y = x_start
            wall_normal = Vector((-1, 0, 0))
        else: # right
            curr_x = l/2.0 + 0.02
            curr_y = x_start
            wall_normal = Vector((1, 0, 0))

        curr_p = Vector((curr_x, curr_y, curr_z))

        step_len = 0.16
        steps = int(h / step_len) + 5

        for s in range(steps):
            next_z = curr_p.z + step_len * random.uniform(0.7, 1.2)
            if next_z > h:
                break

            if side in ['front', 'back']:
                next_x = curr_p.x + random.uniform(-0.12, 0.12)
                next_y = curr_p.y + random.uniform(-0.01, 0.01)
            else:
                next_x = curr_p.x + random.uniform(-0.01, 0.01)
                next_y = curr_p.y + random.uniform(-0.12, 0.12)

            next_p = Vector((next_x, next_y, next_z))

            add_stem_segment(curr_p, next_p)

            if random.random() < 0.85:
                add_leaf(next_p, wall_normal)
                if random.random() < 0.5:
                    add_leaf(next_p + Vector((0,0,0.05)), wall_normal)

            curr_p = next_p

        if side in ['front', 'back']:
            slope_side = -1 if side == 'front' else 1
            slope_angle = math.atan((ph - h) / (w / 2.0))
            roof_normal = Vector((0, -slope_side * math.sin(slope_angle), math.cos(slope_angle)))

            roof_steps = 14
            for rs in range(roof_steps):
                dist_to_peak = abs(curr_p.y)
                if dist_to_peak < 0.1:
                    break

                step_y = 0.18 * math.cos(slope_angle)
                next_y = curr_p.y + slope_side * step_y
                if (slope_side == -1 and next_y > 0.0) or (slope_side == 1 and next_y < 0.0):
                    next_y = 0.0

                t_slope = 1.0 - (abs(next_y) / (w/2.0))
                next_z = h + t_slope * (ph - h) + random.uniform(0.01, 0.04)
                next_x = curr_p.x + random.uniform(-0.12, 0.12)

                next_p = Vector((next_x, next_y, next_z))

                add_stem_segment(curr_p, next_p)
                add_leaf(next_p, roof_normal)
                if random.random() < 0.7:
                    add_leaf(next_p, roof_normal)

                curr_p = next_p

    # Compile mesh in Blender
    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()

    mat_foliage = bpy.data.materials.get("Foliage")
    if mat_foliage:
        obj.data.materials.append(mat_foliage)

    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

print("World-transformed Ivy generator ready!")

Convert Ivy generator to world-coordinate transformer of local growth logic


Code executed successfully: World-transformed Ivy generator ready!