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 xaa: 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.56: 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)