```python
import bpy, math, random, os
import numpy as np
from mathutils import Vector

# THE LANTERNWOOD — a numerically grown, inhabited fungal canopy.
# All of the gill relief is geometry, not a texture or a normal-map.
random.seed(81)
rng = np.random.default_rng(81)
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for d in bpy.data.materials:
    bpy.data.materials.remove(d)
os.makedirs('/mnt/data', exist_ok=True)

def material(name, color, rough=.5, metallic=0, emission=None, strength=0, sss=0):
    m = bpy.data.materials.new(name)
    m.diffuse_color = (*color, 1)
    m.use_nodes = True
    p = m.node_tree.nodes.get('Principled BSDF')
    p.inputs['Base Color'].default_value = (*color, 1)
    p.inputs['Roughness'].default_value = rough
    p.inputs['Metallic'].default_value = metallic
    p.inputs['Subsurface Weight'].default_value = sss
    if emission:
        p.inputs['Emission Color'].default_value = (*emission, 1)
        p.inputs['Emission Strength'].default_value = strength
    return m

def textured(m, scale, strength, distance, c1=None, c2=None):
    n = m.node_tree.nodes
    l = m.node_tree.links
    p = n.get('Principled BSDF')
    tex = n.new('ShaderNodeTexNoise')
    tex.inputs['Scale'].default_value = scale
    tex.inputs['Detail'].default_value = 3.5
    bump = n.new('ShaderNodeBump')
    bump.inputs['Strength'].default_value = strength
    bump.inputs['Distance'].default_value = distance
    l.new(tex.outputs['Fac'], bump.inputs['Height'])
    l.new(bump.outputs['Normal'], p.inputs['Normal'])
    if c1 and c2:
        ramp = n.new('ShaderNodeValToRGB')
        ramp.color_ramp.elements[0].position = .18
        ramp.color_ramp.elements[0].color = (*c1, 1)
        ramp.color_ramp.elements[1].position = .83
        ramp.color_ramp.elements[1].color = (*c2, 1)
        l.new(tex.outputs['Fac'], ramp.inputs[0])
        l.new(ramp.outputs[0], p.inputs['Base Color'])

capmat = material('Dry cinnamon leather • living cap', (.29,.095,.035), .73, sss=.065)
textured(capmat, 4.6, .28, .085, (.11,.028,.012), (.48,.22,.075))
stemmat = material('Ivory ochre • fibrous stipe', (.42,.29,.13), .67, sss=.09)
textured(stemmat, 7, .35, .09, (.20,.12,.048), (.57,.43,.24))
rim = material('Rolled pale growing margin', (.37,.51,.30), .44, sss=.11)
bark = material('Ancient copper bark', (.15,.054,.019), .91)
textured(bark, 3.2, .65, .2, (.035,.012,.006), (.25,.092,.024))
wood = material('Honey weathered planks', (.28,.12,.037), .79)
textured(wood, 6, .3, .06)
wood2 = material('Fresh plank edges', (.44,.235,.079), .7)
darkwood = material('Dark structural timber', (.09,.036,.013), .8)
roofmats = [material('Copper leaf roofing %02d'%i, c, .64, .12)
            for i,c in enumerate([(.29,.078,.025),(.43,.14,.035),(.21,.048,.018),(.35,.12,.055)])]
rope = material('Flax rope', (.37,.25,.12), .95)
windowmat = material('Amber inhabited windows', (.95,.43,.055), .3,
                     emission=(1,.28,.025), strength=4)
gold = material('Lantern brass', (.37,.17,.032), .35, .65)
mossmat = material('Velvet bronze moss', (.16,.18,.041), .95)
fernmat = material('Copper fern blades', (.31,.14,.028), .7)
fernlight = material('Fern young gold tips', (.46,.245,.055), .69)
soil = material('Wet forest humus', (.065,.028,.015), .98)
textured(soil, 2.7, .65, .16, (.017,.009,.005), (.12,.05,.018))
mycelmat = material('Living mycelium threads', (.48,.65,.37), .5,
                    emission=(.15,.55,.32), strength=.35)
sporemat = material('Suspended pale spores', (.47,.85,.59), .4,
                     emission=(.26,1,.62), strength=2.2)
gillmats = []
for i in range(12):
    t = i/11
    c = (.014+.11*t, .105+.43*t, .083+.32*t)
    gillmats.append(material('RD flesh • concentration %02d'%i, c, .34,
                            emission=(.055,.77,.57),
                            strength=.035 + 1.95*t**3, sss=.075))

def mesh(name, verts, faces, mat=None, smooth=False):
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.update()
    ob = bpy.data.objects.new(name, me)
    bpy.context.collection.objects.link(ob)
    if mat: me.materials.append(mat)
    if smooth:
        for p in me.polygons: p.use_smooth = True
    return ob

def cube(name, loc, scale, mat, bevel=0):
    bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
    o = bpy.context.object
    o.name = name
    o.dimensions = scale
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    o.data.materials.append(mat)
    if bevel:
        b = o.modifiers.new('Worn joinery', 'BEVEL')
        b.width = bevel
        b.segments = 2
        o.modifiers.new('Corner normals', 'WEIGHTED_NORMAL')
    return o

def tube(name, pts, radii, mat, sides=8):
    pts = [Vector(p) for p in pts]
    if isinstance(radii, (float,int)): radii = [radii]*len(pts)
    vs, fs = [], []
    for i,p in enumerate(pts):
        tangent = pts[min(i+1,len(pts)-1)] - pts[max(0,i-1)]
        tangent.normalize()
        ref = Vector((0,0,1)) if abs(tangent.z)<.92 else Vector((0,1,0))
        n = tangent.cross(ref).normalized()
        b = tangent.cross(n).normalized()
        for j in range(sides):
            a = j*2*math.pi/sides
            vs.append(tuple(p+radii[i]*(math.cos(a)*n+math.sin(a)*b)))
    for i in range(len(pts)-1):
        for j in range(sides):
            a = i*sides+j
            b = i*sides+(j+1)%sides
            fs.append((a,b,b+sides,a+sides))
    fs.append(tuple(reversed(range(sides))))
    fs.append(tuple((len(pts)-1)*sides+j for j in range(sides)))
    return mesh(name,vs,fs,mat,True)

def beam(name,a,b,r,mat,sides=8):
    return tube(name,[a,b],r,mat,sides)

def uvball(name, loc, scale, mat, segments=16, rings=8):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, location=loc)
    o = bpy.context.object
    o.name=name
    o.scale=scale
    o.data.materials.append(mat)
    for f in o.data.polygons: f.use_smooth=True
    return o

def point(name,loc,color,power,radius=.5):
    d=bpy.data.lights.new(name,'POINT')
    d.color=color; d.energy=power; d.shadow_soft_size=radius
    o=bpy.data.objects.new(name,d); bpy.context.collection.objects.link(o); o.location=loc
    return o

def area(name,loc,target,color,power,size):
    d=bpy.data.lights.new(name,'AREA')
    d.color=color; d.energy=power; d.shape='DISK'; d.size=size
    o=bpy.data.objects.new(name,d); bpy.context.collection.objects.link(o); o.location=loc
    o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler()
    return o

# ---------------------------------------------------------------------------
# A real two-species iterative reaction-diffusion calculation, executed inside
# Blender. Semi-implicit diffusion on a periodic square domain. The inhibitor
# diffuses sixteen times as rapidly as the activator. Cubic activator saturation
# produces a stable, connected Turing labyrinth rather than independent spots.
#
# A_t = Lap(A) + A - A^3 - B + 0.008
# B_t = 16 Lap(B) + 3 A - 1.5 B
#
# No image texture is used. Final concentrations are sampled directly into
# vertex positions and retained as a named mesh attribute.
# ---------------------------------------------------------------------------
N = 384
A = rng.normal(0,.075,(N,N))
B = np.zeros((N,N))
k = 4*np.sin(np.pi*np.fft.fftfreq(N))**2
lap_eigen = k[:,None] + k[None,:]
dt = .20
denA = 1+dt*lap_eigen
denB = 1+dt*16*lap_eigen
for step in range(1150):
    reactionA = A + dt*(A-A*A*A-B+.008)
    reactionB = B + dt*(3*A-1.5*B)
    A = np.fft.ifft2(np.fft.fft2(reactionA)/denA).real
    B = np.fft.ifft2(np.fft.fft2(reactionB)/denB).real
low, high = np.percentile(A,[2,98])
field = np.clip((A-low)/(high-low+1e-9),0,1)

record = bpy.data.texts.new('READ ME — the biological relief')
record.write("""THE LANTERNWOOD
The underside is a continuous displaced mesh, not a painted pattern.
An activator / fast inhibitor reaction-diffusion system was integrated for
1150 steps on a 384 x 384 periodic domain in Blender's Python environment.
Equations:
dA/dt = Lap(A) + A - A^3 - B + 0.008
dB/dt = 16 Lap(B) + 3A - 1.5B
dt = 0.20; semi-implicit Fourier diffusion; seeded random initial activator.
The final activator is stored as the vertex attribute RD_activator.
A continuous radial growth-coordinate warp compresses the labyrinth towards
the margins. At the stipe, anisotropic primary folds merge into that field.
Concentration controls both actual depth and bioluminescent flesh.
All bridges, foliage, spores, mycelium and luminous ridges are real meshes.
""")

def sample_rd(x,y):
    xx=(x*.465+.5)*(N-1)
    yy=(y*.465+.5)*(N-1)
    ix=np.floor(xx).astype(np.int32)%N
    iy=np.floor(yy).astype(np.int32)%N
    fx=xx-np.floor(xx); fy=yy-np.floor(yy)
    return ((1-fx)*(1-fy)*field[iy,ix] +
            fx*(1-fy)*field[iy,(ix+1)%N] +
            (1-fx)*fy*field[(iy+1)%N,ix] +
            fx*fy*field[(iy+1)%N,(ix+1)%N])

def capcoord(t,a):
    r=10*t*(1+.037*np.sin(3*a+.7)+.023*np.sin(7*a)+.013*np.sin(11*a+1))
    x=r*np.cos(a)-.65*(1-t)
    y=r*np.sin(a)+.40*(1-t)
    curl=(1.40*np.maximum(0,np.cos(3*a+.55))**6 +
          .42*np.sin(5*a+.3))*t**7
    base=12.85-.80*t**1.4+curl+.10*np.sin(3*a+.6)*t
    return x,y,base

nr, na = 270, 1152
ts = np.linspace(.068,1,nr)
angles = np.arange(na)*2*np.pi/na
T, TH = np.meshgrid(ts,angles,indexing='ij')
growth=T**1.48
con=sample_rd(growth*np.cos(TH),growth*np.sin(TH))
primary=.5+.5*np.cos(13*TH+1.7*np.sin(3*TH)+5*T)
blend=np.clip((T-.12)/.36,0,1)
blend=blend*blend*(3-2*blend)
con=(1-blend)*primary+blend*con
X,Y,Z=capcoord(T,TH)
edge=np.clip((1-T)/.027,0,1)
height=(.17+.58*con**1.5)*(1.18-.29*T)*edge
Z=Z-height
vertices=np.stack([X,Y,Z],axis=-1).reshape(-1,3).tolist()
faces=[]
for i in range(nr-1):
    for j in range(na):
        nj=(j+1)%na
        faces.append((i*na+j,(i+1)*na+j,(i+1)*na+nj,i*na+nj))
under=mesh('LIVING UNDERSIDE | 1150-step reaction-diffusion relief',vertices,faces,None,True)
for m in gillmats: under.data.materials.append(m)
vals=((con[:-1,:]+con[1:,:]+np.roll(con[:-1,:],-1,axis=1)+np.roll(con[1:,:],-1,axis=1))*.25)
indices=np.clip((vals.reshape(-1)*11).astype(np.int32),0,11)
under.data.polygons.foreach_set('material_index',indices)
attr=under.data.attributes.new('RD_activator','FLOAT','POINT')
attr.data.foreach_set('value',con.astype(np.float32).ravel())
under['process']='Iterative two-species reaction diffusion, 1150 steps'
under['physical_relief_depth_metres']=.75
under['growth_mapping']='Anisotropic primary channels to compressed marginal labyrinth'

# Low, leathery top; thickness vanishes into a softly rolled undulating lip.
rt, at=100,576
TT,AA=np.meshgrid(np.linspace(0,1,rt),np.arange(at)*2*np.pi/at,indexing='ij')
xx,yy,zz=capcoord(TT,AA)
zz += .11+1.16*(1-TT**2)**.8
zz += .055*np.sin(AA*9+TT*14)*TT*(1-TT)
topverts=np.stack([xx,yy,zz],axis=-1).reshape(-1,3).tolist()
topfaces=[]
for i in range(rt-1):
    for j in range(at):
        topfaces.append((i*at+j,i*at+(j+1)%at,(i+1)*at+(j+1)%at,(i+1)*at+j))
mesh('Broad low leathery canopy',topverts,topfaces,capmat,True)
lippts=[]
for a in np.linspace(0,2*np.pi,721):
    x,y,z=capcoord(1,a)
    lippts.append((float(x),float(y),float(z+.025)))
tube('Continuous curled growing margin',lippts,.135,rim,10)

# An asymmetric, deeply fibrous column with individually modeled fibers.
def stemcenter(z):
    return Vector((-.72+.29*math.sin(z*.44),.48+.16*math.sin(z*.64),z))
sv,sf=[],[]
layers, sides=90,112
for i in range(layers):
    z=.48+12.12*i/(layers-1)
    c=stemcenter(z)
    t=i/(layers-1)
    r=1.02+.53*math.exp(-t*8)+.34*t**5+.10*math.sin(t*8)
    for j in range(sides):
        a=2*math.pi*j/sides
        rr=r+.075*math.sin(29*a+t*3)+.05*math.sin(17*a-t*5)
        sv.append(tuple(c+Vector((rr*math.cos(a),rr*math.sin(a),0))))
for i in range(layers-1):
    for j in range(sides):
        sf.append((i*sides+j,i*sides+(j+1)%sides,(i+1)*sides+(j+1)%sides,(i+1)*sides+j))
mesh('Off-center fibrous stipe',sv,sf,stemmat,True)
for j in range(46):
    a=2*math.pi*j/46
    pts=[]
    for z in np.linspace(.5,12.45,33):
        t=(z-.5)/12.1
        c=stemcenter(z)
        r=1.04+.53*math.exp(-t*8)+.34*t**5+.10*math.sin(t*8)
        aa=a+.04*math.sin(z*.8+a)
        pts.append(tuple(c+Vector((r*math.cos(aa),r*math.sin(aa),0))))
    tube('Longitudinal stipe fiber',pts,.024,stemmat,5)

# Forest floor, a fallen nurse log, buttresses and exposed mycelium.
vs,fs=[],[]
ng=79
for iy in range(ng):
    y=-24+iy*65/(ng-1)
    for ix in range(ng):
        x=-34+ix*68/(ng-1)
        z=-.3+.20*math.sin(x*.31)*math.cos(y*.38)+.10*math.sin(x*.93+y*.43)
        vs.append((x,y,z))
for iy in range(ng-1):
    for ix in range(ng-1):
        a=iy*ng+ix
        fs.append((a,a+1,a+1+ng,a+ng))
mesh('Unbounded rolling forest humus',vs,fs,soil,True)
logA=Vector((-6.7,1.4,.65)); logB=Vector((4.9,4.9,.9))
tube('Decaying nurse log',[logA,(-3,2.5,.67),(1,3.7,.75),logB],[1.0,1.13,1.02,.85],bark,18)
direction=(logB-logA).normalized()
cut=logA-direction*.025
beam('Exposed broken log end',cut,cut-direction*.045,.90,wood2,32)
basis=direction.cross(Vector((0,0,1))).normalized()
up=direction.cross(basis).normalized()
for rr in [.24,.42,.60,.77]:
    pts=[tuple(cut-direction*.055 + rr*(basis*math.cos(a)+up*math.sin(a)))
         for a in np.linspace(0,2*math.pi,70)]
    tube('Growth rings in nurse log',pts,.022,darkwood,5)
for j in range(22):
    a=j*2*math.pi/22
    offset=basis*math.cos(a)+up*math.sin(a)
    pts=[tuple(logA.lerp(logB,t)+offset*(1.01+.065*math.sin(t*21+j))) for t in np.linspace(.03,.98,19)]
    tube('Splintered log bark',pts,[random.uniform(.055,.10) for _ in pts],bark,6)
for j in range(13):
    a=j*2*math.pi/13
    length=random.uniform(3.2,6.2)
    pts=[]
    for t in np.linspace(0,1,14):
        aa=a+.21*math.sin(t*5+j)
        rr=.8+length*t
        z=.13+1.6*(1-t)**3
        pts.append((-.7+rr*math.cos(aa),.5+rr*math.sin(aa),z))
    tube('Spreading fungal buttress',pts,[.39*(1-i/14)**1.5+.025 for i in range(14)],stemmat,10)
for j in range(75):
    a=random.uniform(0,2*math.pi); r0=random.uniform(1.4,7)
    pts=[]
    for t in np.linspace(0,1,12):
        r=r0+t*random.uniform(1.2,2.3)
        aa=a+.045*math.sin(14*t+j)
        x=-.7+r*math.cos(aa); y=.5+r*math.sin(aa)
        z=.035+.20*math.sin(x*.31)*math.cos(y*.38)+.10*math.sin(x*.93+y*.43)
        pts.append((x,y,z))
    tube('Exposed mycelial rhizomorph',pts,.014+random.random()*.015,mycelmat,5)
    if j%2==0:
        p=Vector(pts[5])
        q=p+Vector((math.cos(a+.8),math.sin(a+.8),0))*.8
        tube('Mycelium bifurcation',[p,p.lerp(q,.5)+Vector((.08,0,.02)),q],[.017,.011,.004],mycelmat,5)

# Old forest trunks: foreground framing and layered, ember-lit background.
trees=[(-12,1,.95,24),(11,4,.91,25),(-8.8,9,.8,26),(6.2,10,.78,27),
       (-15,10,1.0,29),(15,12,1.1,28),(-18,19,.85,31),(-11,21,.8,29),
       (-3,20,.67,30),(4,22,.87,32),(12,24,.85,29),(20,21,1.1,32),
       (-22,29,.85,34),(0,31,.8,33),(22,34,1,35),(-17,-2,.95,27),(16,0,.8,25)]
for idx,(x,y,r,h) in enumerate(trees):
    lean=random.uniform(-.9,.9)
    pts=[(x+lean*(z/h)**1.5,y+.32*math.sin(z*.17+idx),z) for z in np.linspace(0,h,22)]
    tube('Ancient forest trunk %02d'%idx,pts,[r*(1-.43*i/21)*(1+.065*math.sin(i*1.8+idx)) for i in range(22)],bark,14)
    for j in range(7):
        a=j*2*math.pi/7+idx
        pts2=[]
        for z in np.linspace(.3,h*.91,22):
            rr=r*(1-.43*z/h)
            pts2.append((x+lean*(z/h)**1.5+rr*math.cos(a+.025*z),
                         y+.32*math.sin(z*.17+idx)+rr*math.sin(a+.025*z),z))
        tube('Raised ancient bark seam',pts2,.05,bark,5)
    for j in range(5):
        a=j*2*math.pi/5+idx
        rootend=(x+math.cos(a)*r*3.2,y+math.sin(a)*r*3.2,.02)
        tube('Tree buttress root',[(x,y,1.8),(x+math.cos(a)*r,y+math.sin(a)*r,.42),rootend],[r*.38,r*.27,.05],bark,8)
    for j in range(2):
        z=17+j*4+random.random()*3
        a=random.uniform(0,2*math.pi)
        end=(x+math.cos(a)*5,y+math.sin(a)*5,z+2.4)
        tube('High crooked bough',[(x,y,z),(x+math.cos(a)*2.5,y+math.sin(a)*2.5,z+.7),end],[r*.4,.23,.05],bark,9)

# Habitation. Each small six-sided lodge has real window openings, mullions,
# timber framing, a faceted copper roof, a railed deck, and supporting braces.
def lantern(loc, size=.22):
    x,y,z=loc
    cube('Amber lantern glass',(x,y,z),(size*.65,size*.65,size),windowmat,.015)
    for dz in [-size*.57,size*.57]:
        cube('Lantern brass cap',(x,y,z+dz),(size,size,.055),gold,.018)
    for a in range(4):
        aa=a*math.pi/2+math.pi/4
        beam('Lantern cage',(x+size*.40*math.cos(aa),y+size*.40*math.sin(aa),z-size*.5),
             (x+size*.40*math.cos(aa),y+size*.40*math.sin(aa),z+size*.5),.017,gold,5)
    point('Small amber lantern',(x,y,z),(1,.26,.04),27,.24)

def deck(cx,cy,z,rad, name, rails=True):
    # Parallel planks clipped to the circular platform.
    w=.20
    for i in range(int(2*rad/w)+1):
        yy=-rad+i*w
        if abs(yy)>rad-.04: continue
        half=math.sqrt(max(0,rad*rad-yy*yy))
        cube(name+' individual floorboard',(cx,cy+yy,z),(2*half,.183,.13),wood if i%3 else wood2,.018)
    circle=[(cx+rad*math.cos(a),cy+rad*math.sin(a),z-.02) for a in np.linspace(0,2*math.pi,65)]
    tube(name+' perimeter beam',circle,.10,darkwood,7)
    if rails:
        for j in range(18):
            a=2*math.pi*j/18
            # Leave an entrance towards the camera.
            if 12<=j<=14: continue
            x=cx+rad*math.cos(a); y=cy+rad*math.sin(a)
            beam('Deck rail post',(x,y,z),(x,y,z+.68),.046,wood2,6)
        for zoff in [.34,.68]:
            pts=[(cx+rad*math.cos(a),cy+rad*math.sin(a),z+zoff) for a in np.linspace(-.5,3.9,75)]
            tube('Rope deck balustrade',pts,.026,rope,6)

def house(cx,cy,z,rad=1.12,label='Lodge'):
    deck(cx,cy,z,rad+1,label+' deck')
    hh=1.9
    corners=[Vector((cx+rad*math.cos(a),cy+rad*math.sin(a),z+.13)) for a in [j*math.pi/3+math.pi/6 for j in range(6)]]
    for j in range(6):
        a=corners[j]; b=corners[(j+1)%6]
        mid=(a+b)/2; tangent=(b-a).normalized()
        width=(b-a).length
        angle=math.atan2(tangent.y,tangent.x)
        def panel(name,offset,zz,w,h,mat):
            o=cube(name,tuple(mid+tangent*offset+Vector((0,0,zz))),(w,.095,h),mat,.012)
            o.rotation_euler[2]=angle
        # A genuine framed rectangular aperture on every face.
        panel('Lower wall boards',0,.36,width,.72,wood)
        panel('Wall over window',0,1.65,width,.5,wood)
        winw=.55; winh=.68
        sidew=(width-winw)/2
        panel('Window side pier',-(winw+sidew)/2,1.06,sidew,.70,wood2)
        panel('Window side pier',(winw+sidew)/2,1.06,sidew,.70,wood)
        panel('Recessed glowing window',0,1.06,winw,.67,windowmat)
        for off in [-winw/2,0,winw/2]:
            panel('Window vertical mullion',off,1.06,.042,.78,darkwood)
        for zz in [.70,1.06,1.43]:
            panel('Window crosspiece',0,zz,winw+.15,.047,darkwood)
        panel('Projecting window sill',0,.68,winw+.22,.085,wood2)
        beam('Lodge corner post',a,a+Vector((0,0,hh)),.075,darkwood,7)
        for off in np.linspace(-width*.42,width*.42,6):
            p=mid+tangent*off
            beam('Wall plank seam',p+Vector((0,0,.04)),p+Vector((0,0,.68)),.012,darkwood,4)
    # Roof formed from three separately overlapping courses.
    for course in range(3):
        r0=rad*1.40*(1-course*.30)
        r1=rad*1.40*max(.03,1-(course+1)*.30)
        z0=z+hh+.08+course*.30
        z1=z+hh+.08+(course+1)*.30
        for j in range(18):
            a=j*2*math.pi/18
            b=(j+1)*2*math.pi/18
            v=[(cx+r0*math.cos(a),cy+r0*math.sin(a),z0),
               (cx+r0*math.cos(b),cy+r0*math.sin(b),z0),
               (cx+r1*math.cos(b),cy+r1*math.sin(b),z1),
               (cx+r1*math.cos(a),cy+r1*math.sin(a),z1)]
            ob=mesh('Overlapping copper roof shingle',v,[(0,1,2,3)],roofmats[(j+course)%4])
            sol=ob.modifiers.new('Shingle thickness','SOLIDIFY'); sol.thickness=.035
    uvball('Roof finial',(cx,cy,z+hh+1.03),(.105,.105,.19),gold)
    for a in [0,2.1,4.2]:
        x=cx+(rad+.8)*math.cos(a); y=cy+(rad+.8)*math.sin(a)
        beam('Diagonal deck bracket',(cx,cy,z-1.9),(x,y,z-.11),.11,darkwood,8)
    lantern((cx-.65,cy-rad-.45,z+.60),.26)
    # Tiny stovepipe and smoke-black chimney hood.
    beam('Stovepipe',(cx+.65,cy+.25,z+hh+.25),(cx+.65,cy+.25,z+hh+1.35),.10,darkwood,10)
    uvball('Stovepipe rain cap',(cx+.65,cy+.25,z+hh+1.34),(.18,.18,.07),gold)
    point(label+' interior warmth',(cx,cy-.3,z+1.1),(1,.31,.06),55,.7)

house(-10.5,.35,5.05,1.08,'West watch lodge')
house(9.8,3.4,6.0,1.12,'East lantern lodge')
house(-7.5,8.6,7.0,.93,'Rear moss lodge')
house(5.4,9.8,7.55,.88,'Far keeper lodge')
deck(-.6,.4,4.15,2.55,'Stipe transfer balcony',True)
for a in [0,1.6,3.1,4.7]:
    beam('Stipe balcony braces',(-.6,.4,2.35),
         (-.6+2.4*math.cos(a),.4+2.4*math.sin(a),4.0),.12,darkwood,8)
lantern((-.1,-2.05,4.85),.27)

def bridge(name,start,end,width=.74,sag=.8):
    a=Vector(start); b=Vector(end)
    delta=b-a; horizontal=Vector((delta.x,delta.y,0)).normalized()
    side=Vector((-horizontal.y,horizontal.x,0))
    count=int(delta.length/.24)
    def pos(t):
        return a.lerp(b,t)+Vector((0,0,-sag*4*t*(1-t)))
    for i in range(count+1):
        t=i/count
        p=pos(t)
        ob=cube(name+' loose cross plank',p,(width,.205,.085),wood2 if i%4==0 else wood,.014)
        ob.rotation_euler[2]=math.atan2(side.y,side.x)
        if i%4==0:
            for sign in [-1,1]:
                q=p+side*sign*(width*.52)
                beam('Bridge rope upright',q,q+Vector((0,0,.82)),.028,rope,6)
    for sign in [-1,1]:
        for h in [.02,.85]:
            pts=[tuple(pos(t)+side*sign*width*.53+Vector((0,0,h))) for t in np.linspace(0,1,65)]
            tube(name+' continuous suspension rope',pts,.037 if h else .045,rope,7)
bridge('West hanging footbridge',(-8.55,-.05,5.05),(-2.62,-.75,4.15),.78,.76)
bridge('Eastern rope span',(1.35,1.50,4.15),(7.85,3.05,6.0),.76,.70)
bridge('Rear high trail',(-5.7,8.65,7),(3.6,9.6,7.55),.65,.85)

def ladder(a,b,width=.5):
    a,b=Vector(a),Vector(b)
    v=(b-a).normalized()
    side=v.cross(Vector((0,1,0))).normalized()
    for s in [-1,1]:
        beam('Ladder side rail',a+side*s*width/2,b+side*s*width/2,.045,wood2,7)
    count=int((b-a).length/.28)
    for i in range(count+1):
        p=a.lerp(b,i/count)
        beam('Ladder rung',p-side*width*.55,p+side*width*.55,.031,wood2,7)
ladder((-10.4,-2.2,.1),(-10.4,-1.25,5.15),.6)
ladder((1.45,-3.15,.1),(1.20,-1.60,4.20),.58)
# Small front lookout, low enough to leave the entire fungal canopy clear.
deck(5.7,-3.3,2.1,1.10,'Fern lookout')
for dx,dy in [(-.6,-.5),(.6,-.5),(0,.6)]:
    beam('Lookout stilts',(5.7+dx,-3.3+dy,0),(5.7+dx,-3.3+dy,2.05),.08,darkwood)
bridge('Lookout access',(1.35,-1.4,4.15),(5.0,-2.75,2.1),.61,.38)
lantern((5.7,-4.30,2.90),.23)

# Ferns, built as curved fronds with paired pointed leaflets.
def fern(x,y,z,size,rotation):
    verts,faces=[],[]
    for f in range(7):
        angle=rotation+f*2*math.pi/7
        L=size*random.uniform(.74,1.2)
        direction=Vector((math.cos(angle),math.sin(angle),0))
        side=Vector((-math.sin(angle),math.cos(angle),0))
        centers=[]
        for t in np.linspace(0,1,15):
            centers.append(Vector((x,y,z))+direction*(L*t*.84)+Vector((0,0,L*(.10+1.1*t-.83*t*t))))
        tube('Fern arched rachis',centers,[.022*(1-i/16) for i in range(15)],fernlight,5)
        for i in range(2,14):
            t=i/14; p=centers[i]
            ll=L*.22*math.sin(math.pi*t)**.8
            for sign in [-1,1]:
                tip=p+side*(sign*ll)+direction*(L*.095)
                middle=p.lerp(tip,.5)+Vector((0,0,.055*L))
                breadth=direction*(L*.043*(1-t*.65))
                idx=len(verts)
                verts += [tuple(p),tuple(middle+breadth),tuple(tip),tuple(middle-breadth)]
                faces += [(idx,idx+1,idx+2),(idx,idx+2,idx+3)]
    o=mesh('Seven-frond copper forest fern',verts,faces,fernmat)
    o.data.materials.append(fernlight)
    for p in o.data.polygons:
        if random.random()<.24:p.material_index=1
for j in range(68):
    x=random.uniform(-16,16); y=random.uniform(-8,16)
    if x*x+(y-.5)**2<8:continue
    fern(x,y,0,random.uniform(.48,1.40),random.random()*6.28)
for x,y,s in [(-8,-5,1.9),(8,-6,1.8),(-13,-3,1.6),(3,-5,1.5)]:
    fern(x,y,0,s,random.random()*6.28)

# Moss on log and roots; dark, organic clumps, not an artificial display base.
for j in range(90):
    x=random.uniform(-8,8); y=random.uniform(-3,8)
    if random.random()<.40:
        t=random.random()
        p=logA.lerp(logB,t)
        x,y,z=p.x+random.uniform(-.6,.6),p.y+random.uniform(-.6,.6),p.z+.86
    else:z=.04
    uvball('Low velvet moss cushion',(x,y,z),
           (random.uniform(.14,.55),random.uniform(.14,.43),random.uniform(.07,.18)),mossmat,10,6)

# Hanging moss curtains on peripheral boughs, leaving the central relief open.
for anchor in [(-12,1,17), (11,4,17.5),(-14,8,19),(14,10,20),(-8.8,9,18.7)]:
    x,y,z=anchor
    for j in range(13):
        xx=x+random.uniform(-1.7,1.7); yy=y+random.uniform(-.6,.6)
        length=random.uniform(1.1,3.5)
        pts=[(xx+.10*math.sin(t*9+j), yy+.07*math.sin(t*13),z-length*t) for t in np.linspace(0,1,20)]
        tube('Pendant living moss',pts,[.032*(1-i/23) for i in range(20)],mossmat,5)
        for i in [3,6,9,12,15]:
            p=Vector(pts[i])
            beam('Moss branchlet',p,p+Vector((.13*math.sin(j+i),.12*math.cos(i),-.25)),.012,mossmat,4)

# Background copper foliage clusters: distinct leaves with a fine broken edge.
leafverts,leaffaces=[],[]
for j in range(1350):
    x=random.uniform(-24,24); y=random.uniform(8,32); z=random.uniform(20,28)
    if abs(x)<9 and y<15:continue
    p=Vector((x,y,z))
    a=random.uniform(0,math.pi*2)
    long=Vector((math.cos(a),math.sin(a),random.uniform(-.3,.3)))*random.uniform(.20,.55)
    wide=Vector((-math.sin(a),math.cos(a),0))*random.uniform(.08,.20)
    k0=len(leafverts)
    leafverts.extend([tuple(p-long),tuple(p+wide),tuple(p+long),tuple(p-wide),tuple(p+Vector((0,0,.07)))])
    leaffaces.extend([(k0,k0+1,k0+4),(k0+1,k0+2,k0+4),(k0+2,k0+3,k0+4),(k0+3,k0,k0+4)])
mesh('Distant copper forest leaves',leafverts,leaffaces,fernmat)

# Spores are aggregated into a single real mesh for portable GLB geometry.
spv,spf=[],[]
def spore(p,r):
    x,y,z=p; k=len(spv)
    spv.extend([(x+r,y,z),(x-r,y,z),(x,y+r,z),(x,y-r,z),(x,y,z+r*1.2),(x,y,z-r*1.2)])
    spf.extend([(k,k+2,k+4),(k+2,k+1,k+4),(k+1,k+3,k+4),(k+3,k,k+4),
                (k+2,k,k+5),(k+1,k+2,k+5),(k+3,k+1,k+5),(k,k+3,k+5)])
for j in range(650):
    a=random.uniform(0,2*math.pi)
    t=random.uniform(.79,.99)
    x,y,z=capcoord(t,a)
    drop=random.expovariate(1/1.1)
    if j<430:drop*=.32
    spore((float(x)+random.uniform(-.15,.15),float(y),float(z)-.35-drop),random.uniform(.009,.030))
for j in range(100):
    spore((random.uniform(-11,11),random.uniform(-5,10),random.uniform(.5,11)),random.uniform(.014,.035))
mesh('Fine spore fall below the outer margin',spv,spf,sporemat)

# A few small fruiting bodies on the nurse wood reinforce the gigantic scale.
for j in range(13):
    x=random.uniform(-5,4); y=random.uniform(2,4)
    z=1.55; h=random.uniform(.18,.48)
    beam('Nurse-log tiny mushroom stem',(x,y,z),(x,y,z+h),.035,stemmat,7)
    uvball('Tiny amber satellite cap',(x,y,z+h),(.18,.15,.075),capmat)

# Lighting: a warm forest surrounding a restrained turquoise biological lamp.
world=bpy.data.worlds.new('Burnt orange forest air')
world.use_nodes=True
world.node_tree.nodes['Background'].inputs[0].default_value=(.19,.065,.023,1)
world.node_tree.nodes['Background'].inputs[1].default_value=.28
bpy.context.scene.world=world
area('Warm sunset through trunks',(-13,12,23),(0,0,9),(1,.29,.055),6200,10)
area('Amber front canopy skim',(3,-13,20),(0,0,11),(1,.60,.26),2300,9)
area('Low teal raking light',(-8,-4,10.25),(3,2,12.3),(.11,1,.74),1550,5)
area('Opposed soft cyan relief light',(8,4,10.4),(-2,-2,12.2),(.10,.64,.51),1000,4)
area('Low warm architecture fill',(7,-17,8),(0,1,5),(1,.48,.19),1500,9)
area('Deep orange forest backlight',(1,21,13),(0,0,8),(1,.17,.018),4300,8)
for p in [(-5,-3,11),(4,-2,11),(-1,5,11)]:
    point('Bioluminescent bounce',p,(.05,1,.66),170,2.6)

# Soft airborne depth. It is deliberately sparse so it does not wash out relief.
fogmat=bpy.data.materials.new('Sparse rust-orange forest atmosphere')
fogmat.use_nodes=True
nn=fogmat.node_tree.nodes; nn.clear()
out=nn.new('ShaderNodeOutputMaterial')
vol=nn.new('ShaderNodeVolumePrincipled')
vol.inputs['Color'].default_value=(.48,.25,.095,1)
vol.inputs['Density'].default_value=.006
vol.inputs['Anisotropy'].default_value=.28
fogmat.node_tree.links.new(vol.outputs['Volume'],out.inputs['Volume'])
fog=cube('Forest atmosphere volume',(0,13,16),(76,82,42),fogmat)
fog.display_type='WIRE'

# Low three-quarter presentation. The complete canopy is genuinely three
# dimensional and remains intelligible from the reverse and orbit views.
bpy.ops.object.camera_add(location=(19.8,-29.2,3.4))
cam=bpy.context.object
cam.name='Presentation • under the Lanternwood'
target=Vector((0,1.0,9.0))
cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler()
cam.data.lens=45
cam.data.sensor_width=36
cam.data.clip_start=.1
cam.data.clip_end=180
scene=bpy.context.scene
scene.camera=cam
scene.render.engine='CYCLES'
scene.cycles.samples=64
scene.cycles.use_denoising=True
scene.render.resolution_x=1536
scene.render.resolution_y=1536
scene.render.resolution_percentage=100
scene.render.image_settings.file_format='PNG'
scene.render.film_transparent=False
scene.view_settings.view_transform='AgX'
scene.view_settings.look='AgX - Medium High Contrast'
scene.view_settings.exposure=.3
# A minimal optical bloom, retaining a sharp underlying physical structure.
scene.use_nodes=True
nodes=scene.node_tree.nodes
nodes.clear()
rl=nodes.new('CompositorNodeRLayers')
gl=nodes.new('CompositorNodeGlare')
gl.glare_type='FOG_GLOW'
gl.quality='HIGH'
gl.threshold=1.7
gl.mix=-.94
comp=nodes.new('CompositorNodeComposite')
scene.node_tree.links.new(rl.outputs['Image'],gl.inputs['Image'])
scene.node_tree.links.new(gl.outputs['Image'],comp.inputs['Image'])

# Save a full-fidelity native scene and make a small Cycles validation render.
bpy.ops.wm.save_as_mainfile(filepath='/mnt/data/scene.blend')
scene.render.resolution_x=512
scene.render.resolution_y=512
scene.cycles.samples=20
scene.render.filepath='/mnt/data/preview.png'
try:
    bpy.ops.render.render(write_still=True)
except Exception as exc:
    print("Preview render:", exc)
scene.render.resolution_x=1536
scene.render.resolution_y=1536
scene.cycles.samples=64
scene.render.filepath='/mnt/data/lanternwood.png'
bpy.ops.wm.save_as_mainfile(filepath='/mnt/data/scene.blend')
```