Preserve homestead context and garden-centred Blender design
This commit is contained in:
13
source/samford-modular/README.md
Normal file
13
source/samford-modular/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Samford Valley adaptable homestead
|
||||
|
||||
Open `samford-modular-home.blend` in Blender. The main camera is a north-east aerial view; an alternative camera looks into the open living pavilion. Objects are grouped by building, future additions, services, landscape and planting. Materials are procedural, with no external texture downloads required.
|
||||
|
||||
Built scene: 12 × 6 m living pavilion, 8 × 5 m sleeping pavilion, exposed timber structure, six stacking glazed door panels, a 2.4 m north veranda, covered breezeway and accessible service pipes. The sleeping pavilion sits across a 4 m west gap and a 3 m south gap, interpreted as distances between the building edges.
|
||||
|
||||
Homestead additions: 18 roof-mounted photovoltaic panels on north-facing racks, a 12-panel ground array, battery/inverter cabinet, two 2 m diameter × 2 m tall poly rainwater tanks, six raised food beds, a glazed rectangular greenhouse and mature fruit trees.
|
||||
|
||||
Future office, guest pavilion and workshop are represented by translucent wire edges. Concrete pier stubs and post anchors are present at the office and guest locations. The workshop's north edge sits 15 m south of the living pavilion's south wall.
|
||||
|
||||
The accompanying SVG is a top-down solar and site layout. North is +Y; east is +X. Units are metres.
|
||||
|
||||
This is an imagined site and a visual design concept. Off-grid generation, battery storage, array spacing/shading and water yield have not been sized to household demand or a surveyed property.
|
||||
440
source/samford-modular/build_scene.py
Normal file
440
source/samford-modular/build_scene.py
Normal 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)
|
||||
7
source/samford-modular/check_framing.py
Normal file
7
source/samford-modular/check_framing.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import bpy,os
|
||||
s=bpy.context.scene
|
||||
s.render.engine='CYCLES'; s.cycles.device='CPU'; s.cycles.samples=8; s.cycles.use_denoising=True
|
||||
s.render.resolution_x=960; s.render.resolution_y=540; s.render.resolution_percentage=100
|
||||
s.render.filepath=os.path.join(os.path.dirname(__file__),'framing-check.png')
|
||||
bpy.data.objects['Subtle atmosphere over hillside'].hide_render=True
|
||||
bpy.ops.render.render(write_still=True)
|
||||
49
source/samford-modular/create_layout.py
Normal file
49
source/samford-modular/create_layout.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
out=Path(__file__).parent
|
||||
parts=['''<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1050" viewBox="0 0 1400 1050"><rect width="1400" height="1050" fill="#f5f3eb"/><style>text{font-family:Arial,sans-serif;fill:#203e32}.label{font-size:17px;font-weight:600}.small{font-size:14px;fill:#627368}.note{font-size:16px}.future{fill:none;stroke:#76978d;stroke-width:2;stroke-dasharray:7 5}</style><text x="70" y="70" font-size="34" font-weight="600">SAMFORD VALLEY</text><text x="70" y="104" font-size="21">Adaptable homestead · solar and site layout</text><path d="M1290 135V65m-9 15 9-15 9 15" stroke="#203e32" fill="none" stroke-width="2"/><text x="1283" y="52" font-size="19">N</text>''']
|
||||
S=19
|
||||
def xy(x,y): return (70+(x+31)*S,170+(13-y)*S)
|
||||
def rect(cx,cy,w,d,fill,stroke='#52614d',css=''):
|
||||
x,y=xy(cx-w/2,cy+d/2); parts.append(f'<rect x="{x}" y="{y}" width="{w*S}" height="{d*S}" fill="{fill}" stroke="{stroke}" stroke-width="1.5" class="{css}"/>')
|
||||
def text(x,y,t,cl='label',anchor='middle'):
|
||||
xx,yy=xy(x,y); parts.append(f'<text x="{xx}" y="{yy}" text-anchor="{anchor}" class="{cl}">{t}</text>')
|
||||
def line(points,color='#c9c4ad',w=15):
|
||||
pts=' '.join(f'{xy(x,y)[0]},{xy(x,y)[1]}' for x,y in points); parts.append(f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="{w}" stroke-linejoin="round"/>')
|
||||
line([(1,13),(0,9),(0,7),(-3,6.2),(-7,5),(-9,1),(-10,-3),(-13,-4.6)],w=24)
|
||||
line([(0,7),(4,7.6),(8,8.4),(14,8),(19,7),(20,2),(20,-4.6),(18.2,-5.7)],w=16)
|
||||
rect(0,0,12,6,'#d8d0b7'); rect(0,4.2,12,2.4,'#e7deca')
|
||||
rect(-14,-8.5,8,5,'#d8d0b7')
|
||||
rect(-8.6,-1.8,5.2,1.6,'#c2ad89'); rect(-11.2,-3.9,1.6,5.8,'#c2ad89')
|
||||
for y in [-1.38,1.30]:
|
||||
for i in range(9): rect(-4.82+i*1.205,y,1.10,1.65,'#263e4b','#9aa9ac')
|
||||
for y in [-10.3,-13]:
|
||||
for i in range(6): rect(7.2+i*1.21,y,1.10,1.65,'#263e4b','#9aa9ac')
|
||||
for cx,cy,w,d in [(11.3,-.6,7,5),(-25,-8.5,7,5),(0,-21,8,6)]:
|
||||
rect(cx,cy,w,d,'none',css='future')
|
||||
if cy!=-21:
|
||||
for x in [cx-w/2+.3,cx+w/2-.3]:
|
||||
for y in [cy-d/2+.3,cy+d/2-.3]: rect(x,y,.35,.35,'#a5aaa3')
|
||||
for x,y in [(-18.95,-4),(-21.25,-4.2)]:
|
||||
x1,y1=xy(x,y); parts.append(f'<circle cx="{x1}" cy="{y1}" r="{S}" fill="#345c44"/>')
|
||||
for x in [17.8,21.2]:
|
||||
for y in [-2.7,.6,3.9]: rect(x,y,2.35,1.25,'#718454','#a28862')
|
||||
rect(18.2,-8.5,4.3,5,'#d4e4d9','#7b9b8c')
|
||||
for x in [17,19.4]: rect(x,-8.5,.7,4.3,'#7a905c')
|
||||
for x,y,r in [(18,-13.4,1.5),(24,-14.4,1.7),(25,-4.7,1.4)]:
|
||||
xx,yy=xy(x,y); parts.append(f'<circle cx="{xx}" cy="{yy}" r="{r*S}" fill="#b8c69a" stroke="#90a076"/>')
|
||||
text(0,4.0,'NORTH VERANDA · 2.4 m','small')
|
||||
text(0,-4.5,'Living pavilion · 12 × 6 m')
|
||||
text(0,-5.7,'18 roof PV modules','small')
|
||||
text(-14,-8.4,'Sleeping')
|
||||
text(-14,-9.6,'8 × 5 m','small')
|
||||
text(-8.0,-.3,'Breezeway','small')
|
||||
text(11.3,-.5,'Future office','small')
|
||||
text(-25,-8.5,'Future guest','small')
|
||||
text(0,-21,'Future workshop','small')
|
||||
text(10.3,-15.5,'12 ground PV modules','small')
|
||||
text(18.2,-12.5,'Greenhouse','small')
|
||||
text(19.6,6,'Six raised food beds','small')
|
||||
text(-20.6,-1.9,'Two rainwater tanks','small')
|
||||
text(23,-17,'Mango + avocado','small')
|
||||
parts.append('''<line x1="70" y1="915" x2="1330" y2="915" stroke="#c6cbbc"/><text x="70" y="953" class="note">Build now: two pavilions, breezeway, greenhouse, garden beds, tanks and solar.</text><text x="70" y="982" class="note">Dashed outlines: future additions. Concrete pier stubs at office and guest locations.</text><text x="70" y="1013" class="small">Concept layout only. Solar capacity, storage, shading and water yield require site-specific sizing. All dimensions in metres.</text></svg>''')
|
||||
(out/'samford-solar-site-layout.svg').write_text(''.join(parts))
|
||||
45
source/samford-modular/finish_scene.py
Normal file
45
source/samford-modular/finish_scene.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import bpy,bmesh,math,random,os
|
||||
from mathutils import Vector
|
||||
random.seed(523)
|
||||
OUT=os.path.dirname(os.path.abspath(__file__))
|
||||
s=bpy.context.scene
|
||||
# Ensure all closed construction solids and the atmosphere have outward-facing surfaces.
|
||||
fixed=0
|
||||
for o in bpy.data.objects:
|
||||
if o.type=='MESH' and len(o.data.vertices)==8 and len(o.data.polygons)==6:
|
||||
bm=bmesh.new(); bm.from_mesh(o.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(o.data); bm.free(); fixed+=1
|
||||
print('SOLID_NORMALS_CHECKED',fixed,flush=True)
|
||||
def ground(x,y): return -.024*y+.004*x+.12*math.sin(x*.19)*math.cos(y*.14)+.055*math.sin(x*.72+y*.32)
|
||||
# Put both tanks on the north-west side of the sleeping module, visible from the main camera.
|
||||
prefixes=('2000 mm dark green poly rainwater tank','Poly tank','Tank service lid','Accessible tank outlet','Gravel tank pad')
|
||||
for o in bpy.data.objects:
|
||||
if not o.name.startswith(prefixes): continue
|
||||
center=o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
|
||||
shift=Vector((0,4.1,0)) if center.y>-9 else Vector((-2.3,6.4,0))
|
||||
shift.z=ground(center.x+shift.x,center.y+shift.y)-ground(center.x,center.y)
|
||||
o.location+=shift
|
||||
# Loose gravel fragments soften the hard geometry of unsealed paths.
|
||||
vs=[]; fs=[]
|
||||
routes=[([(1,14),(0,9),(0,7),(-3,6.2),(-7,5),(-9,1),(-10,-3),(-13,-4.6)],1.5), ([(0,7),(4,7.6),(8,8.4),(14,8),(19,7),(20,2)],1.1), ([(20,2),(20,-4.6),(18.2,-5.7)],1)]
|
||||
for points,width in routes:
|
||||
for a,b in zip(points,points[1:]):
|
||||
a,b=Vector(a),Vector(b); delta=b-a; side=Vector((-delta.y,delta.x)).normalized()
|
||||
for _ in range(int(delta.length*135)):
|
||||
p=a+delta*random.random()+side*random.uniform(-width*.62,width*.62); x,y=p; z=ground(x,y)+.04; r=random.uniform(.013,.04); k=len(vs)
|
||||
vs.extend([(x-r,y-r,z),(x+r,y-r*.5,z),(x+r*.7,y+r,z),(x-r*.7,y+r*.7,z),(x,y,z+r*.8)])
|
||||
fs.extend([(k,k+1,k+4),(k+1,k+2,k+4),(k+2,k+3,k+4),(k+3,k,k+4)])
|
||||
me=bpy.data.meshes.new('Loose gravel stones'); me.from_pydata(vs,[],fs); me.materials.append(bpy.data.materials['Loose local gravel - fine stones']); ob=bpy.data.objects.new('Scattered loose gravel and irregular path edges',me); bpy.data.collections['07 Terrain and paths'].objects.link(ob)
|
||||
# Modest golden light; a low-density atmosphere scatters it through the canopy.
|
||||
s.world.node_tree.nodes['Background'].inputs['Strength'].default_value=.6
|
||||
s.view_settings.exposure=.3
|
||||
sun=bpy.data.objects['Warm early morning sun from north-east']; sun.data.energy=3.0
|
||||
v=next(n for n in bpy.data.materials['Very light canopy haze'].node_tree.nodes if n.type=='PRINCIPLED_VOLUME'); v.inputs['Density'].default_value=.0012
|
||||
for area in [a for screen in bpy.data.screens for a in screen.areas if a.type=='VIEW_3D']:
|
||||
area.spaces.active.shading.type='SOLID'; area.spaces.active.shading.color_type='MATERIAL'; area.spaces.active.region_3d.view_perspective='CAMERA'
|
||||
bpy.data.objects['Subtle atmosphere over hillside'].hide_set(True)
|
||||
# hide_set affects only the interactive viewport; the atmosphere remains enabled for render.
|
||||
s.render.resolution_percentage=100; s.cycles.samples=96; s.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'))
|
||||
s.cycles.device='CPU'; s.cycles.samples=12; s.render.resolution_percentage=40; s.render.filepath=os.path.join(OUT,'samford-modular-light-check.png')
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print('LIGHTING_CHECK_COMPLETE',flush=True)
|
||||
65
source/samford-modular/refine_scene.py
Normal file
65
source/samford-modular/refine_scene.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import bpy, random, math, os
|
||||
from mathutils import Vector
|
||||
random.seed(119)
|
||||
OUT=os.path.dirname(os.path.abspath(__file__))
|
||||
s=bpy.context.scene
|
||||
# Layer individual leaves into substantial evergreen canopies instead of sparse twigs.
|
||||
for ob in list(bpy.data.objects):
|
||||
if 'individual canopy leaves' not in ob.name: continue
|
||||
old=ob.data
|
||||
verts=[tuple(v.co) for v in old.vertices]; faces=[tuple(p.vertices) for p in old.polygons]; inds=[p.material_index for p in old.polygons]
|
||||
original=list(verts)
|
||||
for k in range(0,len(original),5):
|
||||
center=Vector(original[k+4]); index=inds[(k//5)*4]
|
||||
for repeat in range(6):
|
||||
offset=Vector((random.uniform(-.36,.36),random.uniform(-.36,.36),random.uniform(-.23,.23)))
|
||||
a=random.uniform(-1,1); scale=random.uniform(.95,1.35); j=len(verts)
|
||||
for v in original[k:k+5]:
|
||||
d=(Vector(v)-center)*scale
|
||||
d=Vector((d.x*math.cos(a)-d.y*math.sin(a),d.x*math.sin(a)+d.y*math.cos(a),d.z))
|
||||
verts.append(tuple(center+offset+d))
|
||||
faces.extend([(j,j+1,j+4),(j+1,j+2,j+4),(j+2,j+3,j+4),(j+3,j,j+4)]); inds.extend([index]*4)
|
||||
me=bpy.data.meshes.new(ob.name+' full layered foliage'); me.from_pydata(verts,[],faces); me.update()
|
||||
for m in old.materials: me.materials.append(m)
|
||||
for p,mi in zip(me.polygons,inds): p.material_index=mi
|
||||
ob.data=me; bpy.data.meshes.remove(old)
|
||||
print('CANOPY',ob.name,len(verts),flush=True)
|
||||
# Move two foreground eucalyptus trees aside to leave greenhouse and beds visible.
|
||||
for ob in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
|
||||
if not ob.data or ob.type!='MESH': continue
|
||||
center=sum((Vector(b) for b in ob.bound_box),Vector())/8
|
||||
center=ob.matrix_world@center
|
||||
if center.x>26 and center.y>5: ob.location.x+=17
|
||||
# Soft blue ambient sky with a warm north-east morning sun.
|
||||
world=s.world; nodes=world.node_tree.nodes; bg=nodes.get('Background')
|
||||
for link in list(bg.inputs['Color'].links): world.node_tree.links.remove(link)
|
||||
bg.inputs['Color'].default_value=(.38,.49,.64,1); bg.inputs['Strength'].default_value=.45
|
||||
s.view_settings.exposure=0
|
||||
sun=bpy.data.objects['Warm early morning sun from north-east']; sun.data.energy=2.3; sun.data.color=(1,.78,.53)
|
||||
sun.location=(35,40,23); sun.rotation_euler=(Vector((-15,-20,0))-sun.location).to_track_quat('-Z','Y').to_euler()
|
||||
# Broad overlapping, wooded ridgelines complete the landscape beyond the site.
|
||||
collection=bpy.data.collections['07 Terrain and paths']
|
||||
for row,(y,base,color) in enumerate([(-58,9,(.13,.21,.16)),(-87,17,(.20,.29,.26)),(-125,23,(.29,.37,.36))]):
|
||||
mat=bpy.data.materials.new('Distant wooded ridge '+str(row)); mat.diffuse_color=(*color,1); mat.use_nodes=True; p=mat.node_tree.nodes.get('Principled BSDF'); p.inputs['Base Color'].default_value=(*color,1); p.inputs['Roughness'].default_value=1
|
||||
vs=[]; fs=[]
|
||||
for j in range(9):
|
||||
yy=y+(j-4)*7
|
||||
for i in range(81):
|
||||
x=-180+i*4.5; crest=base+4*math.sin(x*.048+row)+2.2*math.sin(x*.105+row*2)
|
||||
z=crest*max(0,1-((j-4)/4.5)**2)-2
|
||||
vs.append((x,yy,z))
|
||||
for j in range(8):
|
||||
for i in range(80):
|
||||
k=j*81+i; fs.append((k,k+1,k+82,k+81))
|
||||
me=bpy.data.meshes.new('Ridgeline terrain'); me.from_pydata(vs,[],fs); me.materials.append(mat); ob=bpy.data.objects.new('Layered subtropical ridgeline',me); collection.objects.link(ob)
|
||||
for p in me.polygons: p.use_smooth=True
|
||||
# Tone down new timber and roof glare; retain warm structure and silver-grey cladding.
|
||||
for name in ['Silvered hardwood weatherboard - X grain','Silvered hardwood weatherboard - Y grain']:
|
||||
m=bpy.data.materials[name]; ramp=next(n for n in m.node_tree.nodes if n.type=='VALTORGB')
|
||||
ramp.color_ramp.elements[0].color=(.115,.12,.105,1); ramp.color_ramp.elements[1].color=(.30,.30,.26,1)
|
||||
bpy.data.objects['NE aerial - complete adaptable compound'].data.lens=48
|
||||
s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100; s.render.filepath=os.path.join(OUT,'samford-modular-final.png'); s.cycles.samples=96
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-modular-home.blend'))
|
||||
s.cycles.device='CPU'; s.cycles.samples=12; s.render.resolution_percentage=40; s.render.filepath=os.path.join(OUT,'samford-modular-refined-draft.png')
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print('REFINED_DRAFT_COMPLETE',flush=True)
|
||||
9
source/samford-modular/render_detail.py
Normal file
9
source/samford-modular/render_detail.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import bpy,os
|
||||
s=bpy.context.scene
|
||||
s.camera=bpy.data.objects['Alternate - living pavilion and open veranda']
|
||||
s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100
|
||||
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
|
||||
for d in prefs.devices: d.use=d.type=='METAL'
|
||||
s.cycles.device='GPU'; s.cycles.samples=32; s.cycles.adaptive_threshold=.04; s.cycles.use_denoising=True
|
||||
s.render.filepath=os.path.join(os.path.dirname(__file__),'samford-living-pavilion-detail.png')
|
||||
bpy.ops.render.render(write_still=True)
|
||||
38
source/samford-modular/render_final.py
Normal file
38
source/samford-modular/render_final.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import bpy, os, json
|
||||
from mathutils import Vector
|
||||
from bpy_extras.object_utils import world_to_camera_view
|
||||
OUT=os.path.dirname(os.path.abspath(__file__))
|
||||
s=bpy.context.scene
|
||||
if not s.get('Orchard canopy clearance applied'):
|
||||
for ob in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
|
||||
if ob.type!='MESH': continue
|
||||
c=ob.matrix_world@(sum((Vector(v) for v in ob.bound_box),Vector())/8)
|
||||
if 15<c.x<28 and 5<c.y<15:
|
||||
ob.location.y-=23; ob.location.z+=.55
|
||||
s['Orchard canopy clearance applied']=True
|
||||
names=[o.name for o in s.objects]
|
||||
counts={
|
||||
'solar_panels':sum(n.startswith('PV panel glass and cells') for n in names),
|
||||
'rainwater_tanks':sum(n.startswith('2000 mm dark green poly rainwater tank') for n in names),
|
||||
'raised_garden_beds':sum(n.startswith('Raised vegetable bed soil') for n in names),
|
||||
'sliding_door_panels':sum(n.startswith('Sliding glass pane') and o.location.y>3 for n,o in zip(names,s.objects)),
|
||||
'future_pier_stubs':sum('installed concrete pier stub' in n for n in names),
|
||||
}
|
||||
assert counts['solar_panels']==30,counts
|
||||
assert counts['rainwater_tanks']==2,counts
|
||||
assert counts['raised_garden_beds']==6,counts
|
||||
assert counts['sliding_door_panels']==6,counts
|
||||
assert counts['future_pier_stubs']==8,counts
|
||||
s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100
|
||||
s.cycles.samples=96; s.cycles.adaptive_threshold=.025; s.cycles.use_denoising=True
|
||||
prefs=bpy.context.preferences.addons['cycles'].preferences
|
||||
prefs.compute_device_type='METAL'; prefs.get_devices()
|
||||
for d in prefs.devices: d.use=d.type=='METAL'
|
||||
s.cycles.device='GPU' if any(d.type=='METAL' for d in prefs.devices) else 'CPU'
|
||||
s.render.filepath=os.path.join(OUT,'samford-modular-final.png')
|
||||
report={'render_engine':s.render.engine,'resolution':[2560,1440],'counts':counts,'objects':len(s.objects),'packed_external_textures_required':False}
|
||||
with open(os.path.join(OUT,'scene-checks.json'),'w') as f: json.dump(report,f,indent=2)
|
||||
print('SCENE_CHECKS',json.dumps(report),flush=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-modular-home.blend'))
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print('FINAL_RENDER_COMPLETE',flush=True)
|
||||
16
source/samford-modular/scene-checks.json
Normal file
16
source/samford-modular/scene-checks.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"render_engine": "CYCLES",
|
||||
"resolution": [
|
||||
2560,
|
||||
1440
|
||||
],
|
||||
"counts": {
|
||||
"solar_panels": 30,
|
||||
"rainwater_tanks": 2,
|
||||
"raised_garden_beds": 6,
|
||||
"sliding_door_panels": 6,
|
||||
"future_pier_stubs": 8
|
||||
},
|
||||
"objects": 2686,
|
||||
"packed_external_textures_required": false
|
||||
}
|
||||
Reference in New Issue
Block a user