Preserve homestead context and garden-centred Blender design

This commit is contained in:
2026-09-10 20:52:43 +10:00
commit c1369d9ed7
321 changed files with 17843 additions and 0 deletions

View File

@@ -0,0 +1,440 @@
import bpy, math, random, os, sys, json
from mathutils import Vector
from math import sin, cos, pi
random.seed(418)
OUT=os.path.dirname(os.path.abspath(__file__))
bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete(use_global=False)
for d in list(bpy.data.collections): bpy.data.collections.remove(d)
COL={}
for n in ['01 Living pavilion 12 x 6 m','02 Sleeping pavilion 8 x 5 m','03 Covered breezeway','04 Future modules - wire only','05 Water and services','06 Kitchen garden','07 Terrain and paths','08 Native planting','09 Eucalyptus and fruit trees','10 Furniture','11 Camera and atmosphere']:
c=bpy.data.collections.new(n); bpy.context.scene.collection.children.link(c); COL[n[:2]]=c
active='01'
def mesh(name,vs,fs,mat=None):
m=bpy.data.meshes.new(name); m.from_pydata(vs,[],fs); m.update(); o=bpy.data.objects.new(name,m); COL[active].objects.link(o)
if mat: m.materials.append(mat)
return o
cubevs=[(-.5,-.5,-.5),(-.5,-.5,.5),(-.5,.5,-.5),(-.5,.5,.5),(.5,-.5,-.5),(.5,-.5,.5),(.5,.5,-.5),(.5,.5,.5)]
cubefs=[(0,4,6,2),(1,3,7,5),(0,1,5,4),(2,6,7,3),(0,2,3,1),(4,5,7,6)]
cubefs=[tuple(reversed(f)) for f in cubefs]
def box(n,p,s,mat,bev=0):
o=mesh(n,[(v[0]*s[0],v[1]*s[1],v[2]*s[2]) for v in cubevs],cubefs,mat); o.location=p
if bev:
m=o.modifiers.new('Small construction edge','BEVEL'); m.width=bev; m.segments=2
o.modifiers.new('Corner normals','WEIGHTED_NORMAL')
return o
def beam(n,a,b,w,d,mat):
a,b=Vector(a),Vector(b); o=box(n,(a+b)/2,(w,d,(b-a).length),mat); o.rotation_euler=(b-a).to_track_quat('Z','Y').to_euler(); return o
def rod(n,a,b,r,mat,r2=None,sides=10):
a,b=Vector(a),Vector(b); vec=b-a; q=vec.to_track_quat('Z','Y'); r2=r if r2 is None else r2
vs=[tuple(a+q@Vector((cos(i*2*pi/sides)*rr,sin(i*2*pi/sides)*rr,z))) for z,rr in [(0,r),(vec.length,r2)] for i in range(sides)]
fs=[tuple(reversed(range(sides))),tuple(range(sides,sides*2))]+[(i,(i+1)%sides,(i+1)%sides+sides,i+sides) for i in range(sides)]
o=mesh(n,vs,fs,mat)
for p in o.data.polygons: p.use_smooth=len(p.vertices)==4
return o
def mat(n,c,rough=.65,metal=0):
m=bpy.data.materials.new(n); m.diffuse_color=(*c,1); m.use_nodes=True
p=m.node_tree.nodes.get('Principled BSDF'); p.inputs['Base Color'].default_value=(*c,1); p.inputs['Roughness'].default_value=rough; p.inputs['Metallic'].default_value=metal
return m
def textured(n,c1,c2,scale,rough=.65,bump=.06):
m=mat(n,c1,rough); ns=m.node_tree.nodes; ls=m.node_tree.links; p=ns.get('Principled BSDF')
tex=ns.new('ShaderNodeTexCoord'); mp=ns.new('ShaderNodeVectorMath'); mp.operation='MULTIPLY'; mp.inputs[1].default_value=scale; ls.new(tex.outputs['Generated'],mp.inputs[0])
noise=ns.new('ShaderNodeTexNoise'); noise.inputs['Scale'].default_value=3; noise.inputs['Detail'].default_value=4; noise.inputs['Roughness'].default_value=.75; ls.new(mp.outputs[0],noise.inputs['Vector'])
ramp=ns.new('ShaderNodeValToRGB'); ramp.color_ramp.elements[0].position=.22; ramp.color_ramp.elements[0].color=(*c1,1); ramp.color_ramp.elements[1].position=.78; ramp.color_ramp.elements[1].color=(*c2,1); ls.new(noise.outputs['Fac'],ramp.inputs[0]); ls.new(ramp.outputs[0],p.inputs['Base Color'])
b=ns.new('ShaderNodeBump'); b.inputs['Strength'].default_value=.3; b.inputs['Distance'].default_value=bump; ls.new(noise.outputs['Fac'],b.inputs['Height']); ls.new(b.outputs[0],p.inputs['Normal'])
return m
timber=textured('Spotted gum - warm exposed structure',(.19,.095,.035),(.49,.29,.12),(75,75,1.5),.5,.012)
deckmat=textured('Spotted gum decking - longitudinal grain',(.17,.11,.055),(.39,.25,.115),(1.5,65,40),.65,.009)
weatherX=textured('Silvered hardwood weatherboard - X grain',(.19,.185,.158),(.43,.405,.335),(1.5,65,45),.76,.012)
weatherY=textured('Silvered hardwood weatherboard - Y grain',(.19,.185,.158),(.43,.405,.335),(65,1.5,45),.76,.012)
bamboo=textured('Bamboo lining - golden fibres',(.38,.25,.105),(.64,.48,.25),(65,1.5,35),.61,.008)
concrete=textured('Warm oxide polished concrete - fine aggregate',(.33,.275,.205),(.52,.46,.37),(65,65,65),.38,.006)
piermat=textured('Unsealed concrete footings',(.3,.32,.285),(.5,.51,.45),(12,12,12),.84,.018)
roofmat=textured('Pale Colorbond - powder coated steel',(.59,.60,.54),(.72,.72,.65),(70,70,70),.36,.42e-3)
roofmat.node_tree.nodes['Principled BSDF'].inputs['Metallic'].default_value=.45
dark=mat('Blackened steel fixings',(.055,.06,.05),.4,.65)
glass=mat('Clear glazing',(.88,.94,.91),.07); glass.node_tree.nodes['Principled BSDF'].inputs['Transmission Weight'].default_value=1; glass.node_tree.nodes['Principled BSDF'].inputs['IOR'].default_value=1.45
soil=textured('Damp brown garden soil',(.045,.032,.018),(.12,.095,.05),(25,25,12),.95,.04)
gravelmat=textured('Loose local gravel - fine stones',(.22,.205,.155),(.45,.425,.33),(90,90,20),.91,.05)
tankmat=textured('Dark green polyethylene',(.035,.075,.052),(.067,.125,.08),(70,70,20),.48,.001)
copper=mat('Exposed copper service pipes',(.36,.17,.065),.36,.7)
fabric=textured('Natural linen upholstery',(.39,.37,.29),(.58,.55,.44),(90,90,90),.94,.003)
leafm=[mat('Leaf '+str(i),c,.68) for i,c in enumerate([(.08,.16,.065),(.14,.24,.085),(.21,.29,.11),(.11,.22,.15),(.25,.32,.14)])]
for m in leafm:
m.node_tree.nodes['Principled BSDF'].inputs['Subsurface Weight'].default_value=.06
bark=textured('Eucalyptus peeling pale bark',(.27,.26,.21),(.58,.57,.46),(18,18,1.1),.93,.025)
grassm=[mat('Native grass '+str(i),c) for i,c in enumerate([(.17,.22,.075),(.24,.29,.10),(.30,.30,.14),(.11,.19,.065)])]
debrism=[mat('Fallen leaves '+str(i),c) for i,c in enumerate([(.18,.09,.03),(.29,.19,.07),(.34,.29,.14)])]
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
F=1.12
def floor(cx,cy,w,d):
box('Insulated concrete floor slab',(cx,cy,F-.14),(w,d,.28),concrete,.025)
for x in [cx-w/2+.35,cx,cx+w/2-.35]:
for y in [cy-d/2+.35,cy+d/2-.35]:
z=ground(x,y); box('Concrete pier',(x,y,(z+F-.28)/2),(.35,.35,F-.28-z),piermat,.025)
box('Accessible galvanised post shoe',(x,y,F-.30),(.26,.26,.14),dark,.012)
def shell(cx,cy,w,d,northh,southh,fullopen):
floor(cx,cy,w,d)
yn=cy+d/2; ys=cy-d/2
# Exposed vertical posts; a simple layered wall body represents insulated panel construction.
box('South insulated timber wall',(cx,ys+.09,F+southh/2),(w,.18,southh),weatherX)
for i in range(int(southh/.15)):
box('South hardwood weatherboard',(cx,ys-.027,F+.075+i*.15),(w,.065,.142),weatherX,.004)
for x in [cx-w/2,cx+w/2]:
# Rectangular strips with sloped top ends, no ornamental geometry.
vs=[(x-.09,ys,F),(x-.09,yn,F),(x-.09,yn,F+northh),(x-.09,ys,F+southh),(x+.09,ys,F),(x+.09,yn,F),(x+.09,yn,F+northh),(x+.09,ys,F+southh)]
endwall=mesh('Insulated end wall',vs,[(0,3,2,1),(4,5,6,7),(0,4,7,3),(1,2,6,5),(2,3,7,6)],weatherY)
service_door=fullopen and x<cx
if service_door:
cutter=box('Temporary west door opening',(x,-1.8,F+1.15),(.6,1.12,2.3),None)
mod=endwall.modifiers.new('West entrance opening','BOOLEAN'); mod.operation='DIFFERENCE'; mod.object=cutter
bpy.context.view_layer.objects.active=endwall; endwall.select_set(True); bpy.ops.object.modifier_apply(modifier=mod.name); endwall.select_set(False); bpy.data.objects.remove(cutter,do_unlink=True)
for i in range(int(northh/.15)):
z=.075+i*.15
start=ys if z<=southh else ys+(z-southh)/(northh-southh)*d
if start<yn:
spans=[(start,yn)]
if service_door and z<2.3: spans=[(start,-2.36),(-1.24,yn)]
for aa,bb in spans:
if bb>aa: box('End wall silvered weatherboard',(x+(.115 if x>cx else -.115),(aa+bb)/2,F+z),(.065,bb-aa,.142),weatherY,.004)
if service_door:
for y in [-2.39,-1.21]: box('West entrance jamb',(x-.025,y,F+1.17),(.22,.065,2.34),timber)
box('West entrance lintel',(x-.025,-1.8,F+2.34),(.22,1.25,.08),timber)
for x in [cx-w/2,cx+w/2]:
for y,h in [(ys,southh),(yn,northh)]: box('Exposed spotted gum corner post',(x,y,F+h/2),(.125,.125,h),timber,.005)
if not fullopen:
# North facade assembled around generous bedroom glazing.
for x,ww in [(cx-w/2+.55,1.1),(cx+w/2-2.025,.75),(cx+w/2-.275,.55)]:
box('Bedroom north wall',(x,yn,F+northh/2),(ww,.19,northh),weatherX)
for x in [cx+w/2-1.63,cx+w/2-.57]: box('Bedroom breezeway entrance jamb',(x,yn,F+1.3),(.065,.22,2.6),timber)
box('Bedroom breezeway door lintel',(cx+w/2-1.1,yn,F+2.63),(1.13,.22,.08),timber)
box('Bedroom north lintel',(cx,yn,F+northh-.22),(w,.20,.44),weatherX)
for x in [cx-w/2+i*.60 for i in range(int(w/.60)+1)]:
beam('Exposed roof rafter',(x,ys-.28,F+southh-.09),(x,yn+.28,F+northh-.09),.065,.19,timber)
for i in range(int(w/.14)):
x=cx-w/2+.07+i*.14
beam('Bamboo ceiling lining board',(x,ys,F+southh+.015),(x,yn,F+northh+.015),.135,.019,bamboo)
return yn,ys
def roof(cx,cy,w,d,nh,sh,over=.4):
yn=cy+d/2+over; ys=cy-d/2-over
def z(y): return F+sh+(y-(cy-d/2))/d*(nh-sh)+.14
nx=int((w+2*over)/.09)*8; vs=[]
for i in range(nx+1):
x=cx-w/2-over+(w+2*over)*i/nx
for y in [ys,yn]: vs.append((x,y,z(y)+.011*sin(i*2*pi/8)))
mesh('Continuous corrugated pale metal roof',vs,[(2*i,2*i+2,2*i+3,2*i+1) for i in range(nx)],roofmat)
for x in [cx-w/2-over,cx+w/2+over]: beam('Plain roof edge flashing',(x,ys,z(ys)),(x,yn,z(yn)),.05,.09,roofmat)
for y in [ys,yn]: box('Roof edge',(cx,y,z(y)),(w+2*over,.06,.09),roofmat)
box('Rectangular low edge gutter',(cx,ys-.025,z(ys)-.035),(w+2*over,.16,.12),roofmat)
for y in [cy-d/2+i*1.2 for i in range(int(d/1.2)+1)]: box('Visible timber purlin',(cx,y,z(y)-.095),(w,.07,.075),timber)
def glazing(cx,y,width,height,bottom=F,stack=1):
for i in range(stack):
yy=y+i*.10; box('Sliding glass pane',(cx,yy,bottom+height/2),(width-.10,.016,height-.10),glass)
for x in [cx-width/2,cx+width/2]: box('Spotted gum sliding door stile',(x,yy,bottom+height/2),(.055,.070,height),timber,.004)
for z in [bottom+.035,bottom+height-.035]: box('Sliding door top and bottom rail',(cx,yy,z),(width,.07,.07),timber,.004)
box('Recessed door pull',(cx-width/2+.09,yy+.045,bottom+1.1),(.025,.025,.22),dark,.003)
box('Exposed door track',(cx,y-.05,bottom-.01),(width+.05,.15,.026),dark)
active='01'; shell(0,0,12,6,3.4,2.7,True); roof(0,0,12,6,3.4,2.7)
box('North opening full width header',(0,3,F+3.24),(12,.20,.23),timber)
box('Six panel full width sliding track',(0,3.015,F+.014),(11.85,.65,.03),dark)
glazing(4.97,3.04,1.92,3.09,stack=6)
# 2.4 m veranda. The canopy follows the same practical roof language, slightly falling north.
floor(0,4.2,12,2.4)
for x in [-5.93,-2,2,5.93]:
box('125 x 125 mm spotted gum veranda post',(x,5.32,F+1.47),(.125,.125,2.94),timber,.005)
box('Veranda post base shoe',(x,5.32,F+.065),(.15,.15,.13),dark,.006)
for z in [F+.1,F+2.84]: rod('Visible structural bolt',(x-.07,5.32,z),(x+.07,5.32,z),.012,dark,sides=8)
box('Veranda exposed front beam',(0,5.32,F+2.91),(12.1,.125,.22),timber)
for x in [-5.9+i*.6 for i in range(21)]: beam('Veranda visible rafter',(x,2.98,F+3.27),(x,5.6,F+2.92),.065,.17,timber)
roof(0,4.2,12,2.4,2.95,3.28,over=.20)
for i in range(4): box('Loose wide entry step',(0,5.7+i*.40,F-.22-i*.22),(3.6,.43,.17),concrete,.015)
active='02'; shell(-14,-8.5,8,5,3.4,2.7,False); roof(-14,-8.5,8,5,3.4,2.7)
glazing(-14.45,-5.98,4.65,2.95,stack=1)
box('Bedroom bathroom partition',(-11.8,-8.8,F+1.35),(.12,4.35,2.7),bamboo)
box('Bed timber base',(-14.7,-8.65,F+.23),(2.0,2.25,.40),timber,.02)
box('Bed linen mattress',(-14.7,-8.6,F+.51),(1.9,2.15,.20),fabric,.07)
for x in [-15.2,-14.25]: box('Linen pillow',(x,-9.3,F+.68),(.7,.4,.13),fabric,.07)
# Breezeway terminates at the bedroom north entrance and living west service entrance.
active='03'
for cx,cy,w,d in [(-8.6,-1.8,5.2,1.6),(-11.2,-3.9,1.6,5.8)]:
for i in range(int(w/.14)):
box('Breezeway removable deck board',(cx-w/2+.07+i*.14,cy,F-.03),(.132,d,.065),deckmat,.003)
box('Breezeway floor bearer',(cx,cy,F-.18),(w,d,.20),timber)
box('Breezeway rectangular metal canopy',(cx,cy,F+2.68),(w+.24,d+.24,.07),roofmat,.012)
for x,y in [(cx-w/2+.1,cy-d/2+.1),(cx+w/2-.1,cy+d/2-.1)]:
box('Breezeway post',(x,y,F+1.3),(.125,.125,2.6),timber,.004)
z=ground(x,y); box('Breezeway pier',(x,y,(F+z)/2),(.26,.26,F-z),piermat,.01)
active='10'
# One undivided room: kitchen south-west, dining centre, loose furniture toward valley.
for x in [-4.8,-3.6,-2.4,-1.2]:
box('Simple timber kitchen cabinet',(x,-2.52,F+.44),(1.16,.66,.88),deckmat,.012)
box('Flat cabinet pull',(x,-2.175,F+.76),(.27,.025,.02),dark)
box('Continuous kitchen worktop',(-3,-2.52,F+.93),(5.1,.75,.055),concrete,.012)
box('Stainless sink',(-4.2,-2.5,F+.965),(.70,.48,.025),dark,.04)
rod('Exposed kitchen tap',(-4.2,-2.75,F+.97),(-4.2,-2.75,F+1.3),.018,copper)
rod('Tap spout',(-4.2,-2.75,F+1.3),(-4.2,-2.48,F+1.3),.018,copper)
box('Induction cooktop',(-1.4,-2.52,F+.969),(.6,.52,.019),dark,.01)
box('Open timber kitchen shelf',(-3,-2.75,F+1.83),(4.7,.30,.045),deckmat,.007)
for x in [-4.6,-3.9,-3.1]: rod('Pantry ceramic jar',(x,-2.71,F+1.86),(x,-2.71,F+2.05),.085,concrete,sides=20)
box('Dining table',(-.2,.3,F+.76),(2.3,1.05,.07),deckmat,.018)
for x in [-1.15,.75]:
for y in [-.10,.70]: box('Dining table square leg',(x,y,F+.37),(.07,.07,.74),timber,.006)
for x in [-.9,.45]:
for y in [-.6,1.2]:
box('Timber dining chair seat',(x,y,F+.43),(.46,.46,.045),deckmat,.008)
for dx in [-.17,.17]:
for dy in [-.17,.17]: box('Chair leg',(x+dx,y+dy,F+.21),(.038,.038,.42),timber)
box('Chair back',(x,y+(.2 if y>0 else -.2),F+.69),(.46,.035,.32),deckmat,.008)
box('Loose linen sofa base',(3.2,-.6,F+.22),(2.3,.93,.35),timber,.025)
box('Linen sofa cushion',(3.2,-.55,F+.46),(2.22,.90,.22),fabric,.07)
box('Linen sofa back',(3.2,-.94,F+.73),(2.3,.17,.69),fabric,.065)
box('Low rectangular coffee table',(3.1,.85,F+.33),(1.35,.65,.055),deckmat,.015)
for x in [2.58,3.62]: box('Coffee table support',(x,.85,F+.17),(.065,.52,.30),timber)
for x in [-4.5,-3.3]:
box('Veranda bench seat',(x,4.45,F+.42),(.8,.6,.06),deckmat,.014)
for dx in [-.3,.3]: box('Veranda bench leg',(x+dx,4.45,F+.21),(.06,.48,.4),timber)
active='05'
for cx,cy in [(-18.95,-8.1),(-18.95,-10.6)]:
z=ground(cx,cy); box('Gravel tank pad',(cx,cy,z+.05),(2.3,2.3,.1),gravelmat)
rod('2000 mm dark green poly rainwater tank',(cx,cy,z+.1),(cx,cy,z+2.1),1,tankmat,sides=64)
rod('Poly tank flat top',(cx,cy,z+2.1),(cx,cy,z+2.14),.96,tankmat,sides=64)
rod('Tank service lid',(cx+.2,cy,z+2.14),(cx+.2,cy,z+2.2),.24,tankmat,sides=32)
for j in range(12):
zz=z+.23+j*.15
bpy.ops.mesh.primitive_torus_add(major_segments=48,minor_segments=6,major_radius=1.005,minor_radius=.014,location=(cx,cy,zz))
o=bpy.context.object; o.name='Poly tank reinforcing rib'; o.data.materials.append(tankmat)
for c in list(o.users_collection): c.objects.unlink(o)
COL[active].objects.link(o)
rod('Accessible tank outlet',(cx+.93,cy,z+.28),(cx+1.25,cy,z+.28),.04,copper)
for x,y,z in [(-5.9,-3.43,F+2.75),(-17.9,-11.43,F+2.75)]:
rod('Exposed rainwater downpipe',(x,y,z),(x,y,ground(x,y)+.22),.048,roofmat)
for zz in [F+.28,F+.4]: rod('Surface mounted accessible services',(-5.98,-3.15,zz),(5.9,-3.15,zz),.018,copper)
active='04'
ghost=mat('Future volume - translucent fine line',(.53,.73,.73),.4)
ns=ghost.node_tree.nodes; ls=ghost.node_tree.links; ns.clear(); out=ns.new('ShaderNodeOutputMaterial'); tr=ns.new('ShaderNodeBsdfTransparent'); em=ns.new('ShaderNodeEmission'); em.inputs[0].default_value=(.48,.68,.65,1); em.inputs[1].default_value=.6; mix=ns.new('ShaderNodeMixShader'); mix.inputs[0].default_value=.55; ls.new(tr.outputs[0],mix.inputs[1]); ls.new(em.outputs[0],mix.inputs[2]); ls.new(mix.outputs[0],out.inputs['Surface'])
def future(name,cx,cy,w,d,piers):
z=max(ground(cx,cy)+.38,F); low=z+2.7; high=z+3.4
pts=[(cx-w/2,cy-d/2,z),(cx+w/2,cy-d/2,z),(cx+w/2,cy+d/2,z),(cx-w/2,cy+d/2,z),(cx-w/2,cy-d/2,low),(cx+w/2,cy-d/2,low),(cx+w/2,cy+d/2,high),(cx-w/2,cy+d/2,high)]
for a,b in [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)]: beam(name+' - ghost edge',pts[a],pts[b],.026,.026,ghost)
# Sparse secondary lines maintain a visibly wireframe-only future volume.
for t in [.33,.66]:
x=cx-w/2+w*t; beam(name+' - future roof line',(x,cy-d/2,low),(x,cy+d/2,high),.013,.013,ghost)
if piers:
for x in [cx-w/2+.3,cx+w/2-.3]:
for y in [cy-d/2+.3,cy+d/2-.3]:
g=ground(x,y); box(name+' - installed concrete pier stub',(x,y,g+.16),(.35,.35,.32),piermat,.015); box('Future post anchor',(x,y,g+.34),(.15,.15,.075),dark)
future('Future office',11.3,-.6,7,5,True)
future('Future guest pavilion',-25,-8.5,7,5,True)
future('Future workshop',0,-21,8,6,False)
active='07'
terrainmat=textured('Living soil and patchy subtropical ground',(.09,.13,.035),(.22,.25,.095),(18,18,7),.98,.07)
N=150; vs=[]
for j in range(N+1):
y=-85+j*170/N
for i in range(N+1):
x=-95+i*190/N; vs.append((x,y,ground(x,y)))
ob=mesh('Continuous gently north falling hillside',vs,[(j*(N+1)+i,j*(N+1)+i+1,(j+1)*(N+1)+i+1,(j+1)*(N+1)+i) for j in range(N) for i in range(N)],terrainmat)
for p in ob.data.polygons: p.use_smooth=True
def path(n,points,width):
vs=[]
for idx,(x,y) in enumerate(points):
prev=Vector(points[max(0,idx-1)]); nex=Vector(points[min(len(points)-1,idx+1)]); tangent=(nex-prev).normalized(); normal=Vector((-tangent.y,tangent.x))
for side in [-1,1]:
xx=x+normal.x*width/2*side; yy=y+normal.y*width/2*side; vs.append((xx,yy,ground(xx,yy)+.028))
mesh(n,vs,[(2*i,2*i+1,2*i+3,2*i+2) for i in range(len(points)-1)],gravelmat)
path('Unsealed gravel arrival and module connection',[(1,14),(0,9),(0,7),(-3,6.2),(-7,5),(-9,1),(-10,-3),(-13,-4.6)],1.5)
path('Kitchen garden gravel trail',[(0,7),(4,7.6),(8,8.4),(14,8),(19,7),(20,2)],1.1)
active='06'
beds=[]
for x in [17.8,21.2]:
for y in [-2.7,.6,3.9]:
beds.append((x,y)); z=ground(x,y)
for xx in [x-1.15,x+1.15]: box('Raised bed end board',(xx,y,z+.30),(.055,1.25,.54),weatherY,.005)
for yy in [y-.625,y+.625]: box('Raised bed long board',(x,yy,z+.30),(2.35,.055,.54),weatherX,.005)
box('Raised vegetable bed soil',(x,y,z+.43),(2.23,1.15,.14),soil)
# Efficient batched mesh planting. Individual narrow leaves catch the grazing sunlight.
active='08'
gv=[]; gf=[]; gi=[]
def blade(x,y,z,ang,length,width,lean,mi):
k=len(gv); side=Vector((cos(ang),sin(ang),0)); forward=Vector((-sin(ang),cos(ang),0))
base=Vector((x,y,z)); mid=base+forward*lean*.38+Vector((0,0,length*.65)); tip=base+forward*lean+Vector((0,0,length*.83))
for p in [base-side*width*.25,base+side*width*.25,mid-side*width*.5,mid+side*width*.5,tip]: gv.append(tuple(p))
gf.extend([(k,k+1,k+3,k+2),(k+2,k+3,k+4)]); gi.extend([mi,mi])
def forbidden(x,y):
return (-6.5<x<6.5 and -3.6<y<6.7) or (-18.4<x<-9.5 and -11.6<y<-5.5) or (-12.3<x<-5.7 and -6.6<y<-.8) or (16<x<23 and -4<y<5) or (-20.2<x<-17.7 and -12<y<-6.5)
for i in range(14000):
x=random.uniform(-46,39); y=random.uniform(-36,37)
if forbidden(x,y): continue
# Keep gravel paths readable and thin the immediate forecourt.
if (abs(x)<1.1 and 6<y<15) or (-4<x<16 and 6.7<y<8.8): continue
if random.random()<.35 and -8<x<8 and y>6: continue
z=ground(x,y); ln=random.uniform(.12,.42)
for j in range(5): blade(x+random.uniform(-.09,.09),y+random.uniform(-.09,.09),z,random.uniform(0,2*pi),ln*random.uniform(.7,1.2),random.uniform(.012,.025),random.uniform(.08,.25),random.randrange(4))
for x,y in [(-6.9,3),(-7,4.4),(6.7,3),(7.2,4.6),(-17.4,-4.9),(-16,-4.7),(-14,-4.6),(-8.5,-7),(3.7,6.4),(5.4,6.3),(15.8,5.6),(22.8,6)]:
for j in range(85): blade(x+random.uniform(-.2,.2),y+random.uniform(-.2,.2),ground(x,y),random.uniform(0,2*pi),random.uniform(.5,.95),random.uniform(.016,.035),random.uniform(.3,.65),random.randrange(4))
o=mesh('Native grasses and lomandra - individual blade geometry',gv,gf)
for m in grassm: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,gi): p.material_index=idx
# Ground leaf litter: bent lanceolate eucalyptus leaves.
dv=[]; df=[]; di=[]
for i in range(4200):
x=random.uniform(-35,30); y=random.uniform(-24,22)
if forbidden(x,y): continue
z=ground(x,y)+.035; a=random.uniform(0,2*pi); L=random.uniform(.06,.16); W=L*.23; k=len(dv)
for u,v,dz in [(-L,0,0),(0,W,.008),(L,0,.015),(0,-W,.008)]: dv.append((x+u*cos(a)-v*sin(a),y+u*sin(a)+v*cos(a),z+dz))
df.append((k,k+1,k+2,k+3)); di.append(random.randrange(3))
o=mesh('Scattered fallen eucalyptus leaves',dv,df)
for m in debrism: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,di): p.material_index=idx
for i in range(55):
x=random.uniform(-33,28); y=random.uniform(-20,20)
if not forbidden(x,y): rod('Fallen twig',(x,y,ground(x,y)+.03),(x+random.uniform(.15,.65),y+.25,ground(x,y)+.055),.012,bark,r2=.007,sides=5)
active='09'
def tree(cx,cy,h,fruit=False):
z=ground(cx,cy); lean=random.uniform(-.6,.6); trunkh=h*(.53 if fruit else .68)
base=Vector((cx,cy,z)); top=Vector((cx+lean,cy+.15,z+trunkh))
rod('Fruit tree trunk' if fruit else 'Eucalyptus pale trunk',base,top,h*.022,bark,r2=h*.007,sides=14)
lv=[]; lf=[]; li=[]
clusters=20 if fruit else 24
for j in range(clusters):
a=j*2.4; rr=random.uniform(.15,.30)*h; zz=z+h*random.uniform(.62,.92)
end=Vector((cx+cos(a)*rr,cy+sin(a)*rr,zz))
source=base.lerp(top,random.uniform(.55,.98)); elbow=source.lerp(end,.56)+Vector((0,0,.22))
rod('Exposed canopy branch',source,elbow,h*.008,bark,r2=h*.004,sides=8); rod('Fine canopy branch',elbow,end,h*.004,bark,r2=.015,sides=6)
for k in range(170 if fruit else 135):
theta=random.uniform(0,2*pi); u=random.uniform(-1,1); rad=random.random()**(1/3); ss=math.sqrt(1-u*u)
center=end+Vector((cos(theta)*ss*rad*h*.105,sin(theta)*ss*rad*h*.105,u*rad*h*.071))
a2=random.uniform(0,2*pi); length=random.uniform(.10,.22) if not fruit else random.uniform(.10,.17); width=length*(.20 if not fruit else .45)
long=Vector((cos(a2),sin(a2),random.uniform(-1.2,.2))).normalized()*length
side=Vector((-sin(a2),cos(a2),.05))*width
idx=len(lv)
for p in [center-long,center+side,center+long,center-side,center+Vector((0,0,.016))]: lv.append(tuple(p))
lf.extend([(idx,idx+1,idx+4),(idx+1,idx+2,idx+4),(idx+2,idx+3,idx+4),(idx+3,idx,idx+4)]); li.extend([random.randrange(5)]*4)
o=mesh(('Mango / avocado' if fruit else 'Eucalyptus')+' individual canopy leaves',lv,lf)
for m in leafm: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,li): p.material_index=idx
for x,y,h in [(-31,3,14),(-26,8,15),(-21,12,16),(-34,-15,16),(-23,-22,14),(-14,-23,16),(9,-18,15),(18,-17,16),(28,-10,14),(29,10,14),(35,0,17),(-36,20,18),(34,24,16),(-8,-33,17),(20,-31,18),(-39,-29,19),(38,-29,19)]: tree(x,y,h)
for x,y,h in [(18,9.6,6.5),(24,8.6,7),(25,-4.7,6)]: tree(x,y,h,True)
# Bed crops reuse the same individual blades at a smaller scale.
active='06'; gv=[]; gf=[]; gi=[]
for x,y in beds:
z=ground(x,y)+.52
for dx in [-.8,-.4,0,.4,.8]:
for dy in [-.33,.33]:
for k in range(13): blade(x+dx,y+dy,z,random.uniform(0,2*pi),random.uniform(.14,.31),.045,random.uniform(.08,.2),random.randrange(4))
o=mesh('Kitchen garden leafy vegetables',gv,gf)
for m in leafm[:4]: o.data.materials.append(m)
for p,idx in zip(o.data.polygons,gi): p.material_index=idx
active='05'
pv=mat('Solar photovoltaic cells - blue black silicon',(.014,.026,.043),.23,.42)
silver=mat('Solar anodised aluminium frames',(.38,.42,.42),.3,.8)
cellline=mat('Solar cell fine contacts',(.18,.24,.29),.4,.65)
def solar_panel(cx,cy,z,w=1.10,d=1.76):
# North facing rack: high south edge, low north edge, clearly separate from roof pitch.
tilt=math.radians(20); cs=cos(tilt); sn=sin(tilt)
def point(x,y,zz=0): return (cx+x,cy+y*cs,z-y*sn+zz)
mesh('PV panel glass and cells',[point(-w/2,-d/2),point(w/2,-d/2),point(w/2,d/2),point(-w/2,d/2)],[(0,1,2,3)],pv)
for x in [-w/2,w/2]: beam('PV module side frame',point(x,-d/2),point(x,d/2),.027,.035,silver)
for y in [-d/2,d/2]: beam('PV module end frame',point(-w/2,y),point(w/2,y),.027,.035,silver)
for i in range(1,6): beam('Photovoltaic cell columns',point(-w/2+w*i/6,-d/2,.006),point(-w/2+w*i/6,d/2,.006),.003,.003,cellline)
for j in range(1,11): beam('Photovoltaic cell rows',point(-w/2,-d/2+d*j/11,.006),point(w/2,-d/2+d*j/11,.006),.003,.003,cellline)
for y in [-1.38,1.30]:
z=F+2.7+(y+3)/6*.7+.91
for i in range(9):
x=-4.82+i*1.205; solar_panel(x,y,z)
for yy in [y-.64,y+.64]:
rz=F+2.7+(yy+3)/6*.7+.16; pz=z-(yy-y)*math.tan(math.radians(20))-.04
beam('Roof solar rack standoff',(x,yy,rz),(x,yy,pz),.045,.045,silver)
for yy in [y-.58,y+.58]:
zz=z-(yy-y)*math.tan(math.radians(20))-.055; beam('Continuous solar mounting rail',(-5.5,yy,zz),(5.5,yy,zz),.045,.06,silver)
for y in [-10.3,-13.0]:
for i in range(6):
x=7.2+i*1.21; z=ground(x,y)+1.28; solar_panel(x,y,z)
for yy in [y-.65,y+.65]:
beam('Ground array removable steel leg',(x,yy,ground(x,yy)),(x,yy,z-(yy-y)*math.tan(math.radians(20))-.04),.055,.055,dark)
box('Off-grid battery cabinet',(6.42,-1.7,F+.74),(.58,1.06,1.48),roofmat,.025)
box('Battery cabinet service access',(6.73,-1.7,F+.74),(.022,.94,1.31),dark,.018)
box('Solar inverter',(6.38,-.68,F+1.45),(.42,.46,.63),roofmat,.018)
for yy in [-.78,-.61]: rod('Accessible inverter conduit',(6.42,yy,F+1.11),(6.42,yy,F+.22),.019,copper)
active='06'
gx,gy=18.2,-8.5; gz=ground(gx,gy)+.14; gw,gd=4.3,5
box('Greenhouse loose gravel base',(gx,gy,gz-.055),(gw+.4,gd+.4,.11),gravelmat)
# Rectangular lean-to greenhouse, with a simple mono-pitch glazed roof.
for x in [gx-gw/2+i*gw/4 for i in range(5)]:
for y,h in [(gy-gd/2,2.7),(gy+gd/2,2.4)]: box('Greenhouse timber upright',(x,y,gz+h/2),(.065,.065,h),timber,.003)
for y in [gy-gd/2+i*gd/5 for i in range(6)]:
h=2.7-(y-(gy-gd/2))/gd*.3
for x in [gx-gw/2,gx+gw/2]: box('Greenhouse side upright',(x,y,gz+h/2),(.065,.065,h),timber,.003)
beam('Greenhouse exposed roof bar',(gx-gw/2,y,gz+h),(gx+gw/2,y,gz+h),.065,.075,timber)
for x in [gx-gw/2,gx+gw/2]:
for z in [gz+.07,gz+1.15]: box('Greenhouse side rail',(x,gy,z),(.065,gd,.065),timber)
mesh('Greenhouse side glazing',[(x,gy-gd/2,gz+.08),(x,gy+gd/2,gz+.08),(x,gy+gd/2,gz+2.4),(x,gy-gd/2,gz+2.7)],[(0,1,2,3)],glass)
for y,h in [(gy-gd/2,2.7),(gy+gd/2,2.4)]:
box('Greenhouse end glazing',(gx,y,gz+h/2),(gw,.012,h),glass)
for z in [gz+.07,gz+1.15,gz+h]: box('Greenhouse end rail',(gx,y,z),(gw,.065,.065),timber)
mesh('Greenhouse mono-pitch glass roof',[(gx-gw/2,gy-gd/2,gz+2.7),(gx+gw/2,gy-gd/2,gz+2.7),(gx+gw/2,gy+gd/2,gz+2.4),(gx-gw/2,gy+gd/2,gz+2.4)],[(0,1,2,3)],glass)
for x in [gx-gw/2+i*gw/4 for i in range(5)]: beam('Greenhouse sloping roof rafter',(x,gy-gd/2,gz+2.7),(x,gy+gd/2,gz+2.4),.065,.07,timber)
for x in [gx-1.4,gx+1.4]:
box('Greenhouse growing bed',(x,gy,gz+.27),(.85,4.4,.48),weatherY,.01)
box('Greenhouse growing soil',(x,gy,gz+.52),(.78,4.3,.06),soil)
for y in [gy-1.6,gy-.8,gy,gy+.8,gy+1.6]:
rod('Greenhouse tomato stake',(x,y,gz+.55),(x,y,gz+1.95),.015,timber,sides=6)
for zz in [gz+.75,gz+1.0,gz+1.25,gz+1.5]:
for s in [-1,1]:
beam('Climbing food plant stem',(x,y,zz),(x+s*.22,y+.08,zz+.10),.018,.018,leafm[1])
mesh('Greenhouse broad vegetable leaf',[(x,y,zz),(x+s*.16,y-.1,zz+.1),(x+s*.32,y,zz+.08),(x+s*.16,y+.13,zz+.12)],[(0,1,2,3)],leafm[1])
box('Greenhouse door handle',(gx+.43,gy+gd/2+.045,gz+1.1),(.025,.05,.22),dark)
for x in [gx-.48,gx+.48]: box('Greenhouse door frame',(x,gy+gd/2+.025,gz+1.1),(.065,.065,2.2),timber)
path('Greenhouse gravel access',[(20,2),(20,-4.6),(18.2,-5.7)],1.0)
active='11'; scene=bpy.context.scene
world=bpy.data.worlds.new('Subtropical early morning sky'); world.use_nodes=True; scene.world=world
ns=world.node_tree.nodes; ls=world.node_tree.links; sky=ns.new('ShaderNodeTexSky'); sky.sky_type='MULTIPLE_SCATTERING'; sky.sun_elevation=math.radians(12); sky.sun_rotation=math.radians(45); sky.altitude=.12; sky.air_density=1.1; sky.aerosol_density=1.8; sky.sun_disc=False; ls.new(sky.outputs[0],ns['Background'].inputs[0]); ns['Background'].inputs[1].default_value=.22
bpy.ops.object.light_add(type='SUN',location=(35,40,30)); sun=bpy.context.object; sun.name='Warm early morning sun from north-east'; sun.data.energy=2.2; sun.data.angle=math.radians(3); sun.data.color=(1,.74,.45); sun.rotation_euler=(Vector((-15,-20,0))-sun.location).to_track_quat('-Z','Y').to_euler()
vol=bpy.data.materials.new('Very light canopy haze'); vol.use_nodes=True; ns=vol.node_tree.nodes; ns.clear(); v=ns.new('ShaderNodeVolumePrincipled'); v.inputs['Density'].default_value=.0017; v.inputs['Color'].default_value=(.72,.78,.71,1); v.inputs['Anisotropy'].default_value=.35; out=ns.new('ShaderNodeOutputMaterial'); vol.node_tree.links.new(v.outputs['Volume'],out.inputs['Volume']); box('Subtle atmosphere over hillside',(0,0,15),(170,170,38),vol)
bpy.ops.object.camera_add(location=(43,58,30)); cam=bpy.context.object; cam.name='NE aerial - complete adaptable compound'; target=Vector((-5,-6,1.0)); cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler(); cam.data.type='PERSP'; cam.data.lens=46; cam.data.clip_end=350; scene.camera=cam
# A close exterior camera is included for inspecting the open room and exposed structure.
bpy.ops.object.camera_add(location=(16,23,9)); detail=bpy.context.object; detail.name='Alternate - living pavilion and open veranda'; detail.rotation_euler=(Vector((-1,0,2.2))-detail.location).to_track_quat('-Z','Y').to_euler(); detail.data.lens=43
for o in [sun,cam,detail]:
for c in list(o.users_collection): c.objects.unlink(o)
COL['11'].objects.link(o)
scene.render.engine='CYCLES'; scene.cycles.samples=96; scene.cycles.use_denoising=True; scene.cycles.adaptive_threshold=.025
try:
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
devices=[d for d in prefs.devices if d.type=='METAL']
if devices:
for d in prefs.devices: d.use=d.type=='METAL'
scene.cycles.device='GPU'
print('CYCLES_DEVICES',[(d.name,d.type,d.use) for d in prefs.devices],flush=True)
except Exception as e: print('CPU rendering',e,flush=True)
scene.cycles.max_bounces=8; scene.cycles.transmission_bounces=6; scene.cycles.transparent_max_bounces=8; scene.cycles.volume_bounces=1
scene.render.resolution_x=2560; scene.render.resolution_y=1440; 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
scene.unit_settings.system='METRIC'; scene.unit_settings.length_unit='METERS'
scene['Design intent']='Start with two insulated rectangles; live in them first; adapt. Exposed structure and services, no ornament.'
scene['Orientation']='X east, Y north, Z up. Living 12 x 6 m, clear internal roof height north 3.4 m / south 2.7 m. North veranda 2.4 m.'
scene['Site layout assumption']='Sleeping pavilion 8 x 5 m, 4 m clear west gap and 3 m clear south gap from living. Future workshop north edge 15 m south of living south edge.'
scene['Geographic context']='Imagined Samford Valley site, Queensland. Not a survey or a construction design.'
scene['Homestead energy concept']='18 rooftop PV modules on north-facing racks; 12 ground-array modules; accessible battery cabinet and inverter. Visual layout only, system capacity not engineered.'
scene['Food and water']='Six raised vegetable beds; rectangular glazed greenhouse with growing beds; mature fruit trees; two 2 m diameter x 2 m high poly rainwater tanks.'
readme=bpy.data.texts.new('READ ME - Adaptable Samford home'); readme.write('SAMFORD VALLEY / MODULAR HOME\n\nAll buildings are predictions. All predictions are wrong. — Stewart Brand\n\nTwo insulated timber rectangles, north-opening living space and covered breezeway. Structural and service elements are intentionally accessible. Future office, guest and workshop are translucent edges only; office and guest pier stubs are installed.\n\nMetric units. X east, Y north. All materials are procedural and contained in this file. Two cameras included. Main camera: NE aerial.\nSleeping offsets interpreted as clear gaps between building edges.\n')
bpy.ops.object.select_all(action='DESELECT')
for screen in bpy.data.screens:
for area in screen.areas:
if area.type=='VIEW_3D':
area.spaces.active.region_3d.view_perspective='CAMERA'; area.spaces.active.shading.type='MATERIAL'
scene.render.filepath=os.path.join(OUT,'samford-modular-final.png')
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-modular-home.blend'))
# A quick first pass checks framing before committing to the full Cycles render.
scene.render.resolution_percentage=45; scene.cycles.samples=24; scene.render.filepath=os.path.join(OUT,'samford-modular-draft.png')
bpy.ops.render.render(write_still=True)
print('DRAFT_COMPLETE',len(scene.objects),flush=True)