Files

235 lines
19 KiB
Python

import bpy,math,json,os,random
from mathutils import Vector,Matrix
from math import sin,cos,pi
OUT=os.path.dirname(__file__);s=bpy.context.scene;random.seed(904)
detail=bpy.data.collections['DETAIL - full geometry for renders'];fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
col=bpy.data.collections.new('34 Garden court - aligned pavilions and flowing landscape');detail.children.link(col)
COL={'new':col};active='new';src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
cubefs=[tuple(reversed(f)) for f in cubefs]
wood=bpy.data.materials['Unpainted hardwood - long X grain'];woodz=bpy.data.materials['Oiled spotted gum posts - vertical grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes'];stone=bpy.data.materials['Split sandstone facing - mineral grain'];roof=bpy.data.materials['Pale Colorbond - powder coated steel'];gravel=bpy.data.materials['Loose local gravel - fine stones'];soil=bpy.data.materials['Damp brown garden soil'];F=1.12
leafm=[mat('Garden leaf '+str(i),c,.77) for i,c in enumerate([(.12,.24,.085),(.21,.32,.12),(.15,.29,.21),(.28,.36,.15)])]
flowers=[mat('Garden flower '+n,c,.6) for n,c in [('soft ivory',(.89,.86,.72)),('warm lilac',(.45,.23,.55)),('bottlebrush rose',(.60,.10,.19)),('native yellow',(.83,.53,.075))]]
lawn=mat('Courtyard low native groundcover',(.18,.26,.105),.95)
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
def matrix(o):return Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy())
def center(o):return matrix(o)@(sum((Vector(v) for v in o.bound_box),Vector())/8) if o.type=='MESH' else o.location.copy()
dirty=set();removed=[];moved=set()
def delete(o):
dirty.update(c.name for c in o.users_collection);removed.append(o.name);bpy.data.objects.remove(o,do_unlink=True)
def move(o,delta):
if o in moved:return
dirty.update(c.name for c in o.users_collection);o.location+=Vector(delta);moved.add(o)
# Remove the entire play platform and its equipment, old angular paths and links.
for o in list(bpy.data.collections['33 Daily life - training gathering play and food'].objects):
n=o.name
if n.startswith(('PLAY DECK','FOLDING TABLE TENNIS','Table tennis','Play deck','Training to garage pedestrian link','SHADED WALKING LOOP','Walking loop')):delete(o)
elif center(o).x<-7 and center(o).y<-13:move(o,(2.8,-1,0))
for cname in ['20 Spacious site connections','27 Gravel access road and arrival','23 NOW - Quiet meditation room']:
for o in list(bpy.data.collections[cname].objects):
if cname.startswith('27') or o.name in ['Main meandering gravel arrival','Sauna and tanks gravel walk','Garden and greenhouse gravel walk'] or o.name.startswith(('Meditation covered walk','Meditation walk timber')):delete(o)
# Align meditation with parents wing; line the garage up with main pavilion axis.
for cname,delta in [('23 NOW - Quiet meditation room',(-.8,0,0)),('28 Garage and workbench',(3,0,0)),('29 Car and bicycles',(3,0,0)),('16 Oval Finnish sauna',(0,2.2,0)),('17 Outdoor shower and stone terrace',(0,2.2,0))]:
for o in bpy.data.collections[cname].objects:move(o,delta)
for o in bpy.data.collections['30 VIEW - Transparent building sides'].objects:
if o.name.startswith('Meditation'):move(o,(-.8,0,0))
elif o.name.startswith('Garage'):move(o,(3,0,0))
elif o.name.startswith('Sauna'):move(o,(0,2.2,0))
for o in bpy.data.collections['20 Spacious site connections'].objects:
if o.name.startswith('Meditation'):move(o,(-.8,0,0))
# Shift only ground-mounted solar; retain roof PV, tanks and battery cabinet.
for o in bpy.data.collections['05 Water and services'].objects:
p=center(o)
if p.x>8 and p.y<-12:move(o,(-3.8,-1.3,0))
# Relocate two concept eucalyptus groups clear of solar and the curved driveway.
tree_delta=None
for o in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
if o.name.startswith(('Eucalyptus pale trunk','Fruit tree trunk')):
tree_delta=(-1,-11,0) if o.name=='Eucalyptus pale trunk.006' else ((7,-1,0) if o.name=='Eucalyptus pale trunk.007' else None)
if tree_delta:move(o,tree_delta)
# Recompose future shadow modules as a mirrored partner and an axial northern reserve.
for o in bpy.data.collections['24 LATER - Shadow spaces only'].objects:
p=center(o);move(o,(14.8,-5,0) if p.x<1 else (-7,9,0))
# Smooth cubic paths, sampled once and shared with the plan drawing.
paths=[];beds=[]
def smooth(points,closed=False,n=20):
p=[Vector(q) for q in points];out=[]
for i in range(len(p) if closed else len(p)-1):
a=p[(i-1)%len(p)] if closed or i else p[0];b=p[i];c=p[(i+1)%len(p)];d=p[(i+2)%len(p)] if closed else p[min(i+2,len(p)-1)]
for j in range(n):
t=j/n;v=.5*((2*b)+(-a+c)*t+(2*a-5*b+4*c-d)*t*t+(-a+3*b-3*c+d)*t*t*t);out.append(tuple(v))
out.append(out[0] if closed else tuple(p[-1]));return out
def ribbon(name,pts,w,material,z=None,record=True):
vs=[];fs=[]
for i,p in enumerate(pts):
p=Vector(p);d=Vector(pts[min(i+1,len(pts)-1)])-Vector(pts[max(0,i-1)]);side=Vector((-d.y,d.x)).normalized()*w/2
for q in [p-side,p+side]:vs.append((q.x,q.y,ground(q.x,q.y)+.085 if z is None else z))
for i in range(len(pts)-1):k=i*2;fs.append((k,k+1,k+3,k+2))
ob=mesh(name,vs,fs,material)
if record:paths.append({'name':name,'points':pts,'width':w,'kind':'roofed walk' if z is not None else 'gravel'})
return ob
def route(name,points,w=1.35,closed=False):return ribbon(name,smooth(points,closed),w,gravel)
def ellipse(cx,cy,rx,ry,n=128):return [(cx+rx*cos(i*2*pi/n),cy+ry*sin(i*2*pi/n)) for i in range(n+1)]
def patch(name,pts,material):
cx=sum(p[0] for p in pts[:-1])/(len(pts)-1);cy=sum(p[1] for p in pts[:-1])/(len(pts)-1)
vs=[(cx,cy,ground(cx,cy)+.045)]+[(x,y,ground(x,y)+.045) for x,y in pts[:-1]]
return mesh(name,vs,[(0,i+1,(i+1)%(len(vs)-1)+1) for i in range(len(vs)-1)],material)
# Central oval garden with balanced paired flower beds and clear north view.
patch('CENTRAL GARDEN - low meadow oval',ellipse(0,13.2,7.8,4.6),lawn)
ribbon('GARDEN COURT - continuous oval gravel walk',ellipse(0,13.2,8.5,5.3),1.35,gravel)
# Covered curving timber connection to the existing meditation entry.
walk=smooth([(-3,5.4),(-4,6.6),(-7.8,7.0),(-10.8,7.9),(-11.8,9.6)])
ribbon('CURVED BREEZEWAY - timber deck',walk,1.35,wood,F)
ribbon('Curved breezeway pale roof',walk,1.55,roof,F+2.60,False)
for i in range(0,len(walk),10):
x,y=walk[i];p=Vector(walk[min(i+1,len(walk)-1)])-Vector(walk[max(0,i-1)]);side=Vector((-p.y,p.x)).normalized()
for sign in [-1,1]:
q=Vector((x,y))+side*.61*sign;g=ground(*q);box('Curved walk timber post',(q.x,q.y,(g+F+2.60)/2),(.085,.085,F+2.6-g),woodz,.006)
beam('Curved walk radial rafter',(x-side.x*.78,y-side.y*.78,F+2.56),(x+side.x*.78,y+side.y*.78,F+2.56),.055,.13,wood)
for i in range(0,len(walk)-1,2):
x,y=walk[i];p=Vector(walk[i+1])-Vector(walk[i]);side=Vector((-p.y,p.x)).normalized();beam('Curved walkway individual deck board',(x-side.x*.66,y-side.y*.66,F-.025),(x+side.x*.66,y+side.y*.66,F-.025),.115,.05,wood)
# Broad, shallow stair centred on the main pavilion.
for i in range(8):
top=F-.16*(i+1);box('Garden axis broad timber stair',(0,5.57+i*.30,top-.075),(2.4,.32,.15),wood,.008)
route('Garden court east future threshold',[(8.5,13.2),(9.55,13.2)])
route('Northern axial future path',[(0,18.5),(0,20),(0,21.6)])
route('Wellness garden curve',[(-7,16.2),(-12,17),(-17,16),(-18,10),(-20,8.3),(-23,8.1),(-23,7.3)],1.4)
route('Rainwater service garden path',[(-23,8.1),(-28.5,8.2),(-29.3,1),(-28,-5),(-25,-8)],1.15)
route('Eastern garden promenade',[(7,16.2),(12,17),(17,16),(20,11.5),(21,7),(21,1),(21,-5),(24.2,-4.8)],1.4)
route('Orchard quiet loop',[(20,11.5),(27,12.5),(32,10),(33,3),(33,-8),(28,-11),(22,-10),(20,-5),(21,1)],1.25)
route('Dining terrace to food garden',[(15,3.5),(17.5,4.8),(21,4.8)],1.4)
route('Kitchen garden harvest access',[(28.8,4.8),(30.8,4),(31,1.5),(30.2,1.5)],1.0)
# Generous pedestrian arrival curves around the planted former play footprint.
route('Arrival garden promenade',[(3.2,-13.8),(5.6,-10.5),(6.3,-5.2),(10,-2),(15,-.2),(15,3.5)],1.5)
route('Training arrival link',[(-5.9,-18),(-5.5,-15.2),(-4.7,-11.3),(-1,-9.8),(5.6,-10.5)],1.25)
for i in range(6):
top=F-.18*(i+1);box('Dining terrace garden access stair',(13.16+i*.30,3.5,top-.08),(.32,1.5,.16),wood,.008)
road=smooth([(36,-40),(28,-33),(20,-27),(16.8,-22),(16,-16),(12,-12.3),(6,-12.3),(0,-12.3)])
ribbon('VEHICLE ACCESS - flowing 4m gravel driveway',road,4,gravel)
apron=smooth([(-4,-14.5),(-3,-15.4),(6,-15.4),(8,-14),(8,-10.4),(6,-9.5),(-3,-9.5),(-4,-10.5)],True)
patch('Rounded gravel garage arrival court',apron,gravel)
# Low and medium native-inspired planting; all beds outside buildings and walking surfaces.
# Geometry is illustrative of form, not a botanical planting specification.
flower_centers=[]
def gardenbed(name,cx,cy,rx,ry,seed):
rng=random.Random(seed);outline=ellipse(cx,cy,rx,ry);patch(name+' mulched soil',outline,soil);beds.append({'name':name,'center':[cx,cy],'radii':[rx,ry]})
# Fine timber edging follows the same soft geometry.
ribbon(name+' curved timber edge',outline,.045,wood,record=False)
vs=[];fs=[];mis=[]
def face(coords,mi):
k=len(vs);vs.extend(coords);fs.append(tuple(range(k,k+len(coords))));mis.append(mi)
for j in range(max(12,int(rx*ry*7))):
a=rng.random()*2*pi;rr=math.sqrt(rng.random())*.91;x=cx+rx*rr*cos(a);y=cy+ry*rr*sin(a);g=ground(x,y)+.06;h=rng.uniform(.2,.62)
flower_centers.append((x,y))
# Narrow arching lomandra/flax leaves.
for k in range(13):
a=rng.random()*2*pi;leng=rng.uniform(.22,.53);w=rng.uniform(.017,.035);dx,dy=cos(a),sin(a)
start=(x,y,g);mid=(x+dx*leng*.4,y+dy*leng*.4,g+h);end=(x+dx*leng,y+dy*leng,g+h*.42)
face([start,(mid[0]-dy*w,mid[1]+dx*w,mid[2]),end,(mid[0]+dy*w,mid[1]-dx*w,mid[2])],rng.randrange(4))
# Fine stems with small flower clusters, varied ivory / lilac / rose / yellow.
for f in range(rng.randint(3,6)):
xx=x+rng.uniform(-.22,.22);yy=y+rng.uniform(-.22,.22);zz=g+h+rng.uniform(.03,.20);c=rng.randrange(4)
face([(xx-.003,yy,g),(xx+.003,yy,g),(xx+.003,yy,zz),(xx-.003,yy,zz)],1)
for petal in range(6):
a=petal*pi/3;length=rng.uniform(.035,.055);ux,uy=cos(a),sin(a);px,py=-uy,ux
face([(xx,yy,zz),(xx+ux*length*.65+px*.020,yy+uy*length*.65+py*.020,zz+.014),(xx+ux*length,yy+uy*length,zz+.005),(xx+ux*length*.65-px*.020,yy+uy*length*.65-py*.020,zz+.014)],4+c)
ob=mesh(name+' native grasses and flowering perennials',vs,fs)
for m in leafm+flowers:ob.data.materials.append(m)
ob.data.polygons.foreach_set('material_index',mis)
# Paired garden beds establish symmetry without a solid wall of planting.
for sign in [-1,1]:
gardenbed(('West' if sign<0 else 'East')+' veranda flower border',sign*3.9,6.2,2.25,.52,100+sign)
gardenbed(('West' if sign<0 else 'East')+' oval court flower crescent',sign*4.2,14.1,2.1,1.12,200+sign)
gardenbed(('West' if sign<0 else 'East')+' axial future garden',sign*3.9,20.9,1.35,.7,300+sign)
gardenbed('FORMER PLAY AREA - flowering kitchen garden',10.9,-6.8,3.1,2.7,404)
gardenbed('Dining garden soft edge',16,7.3,2.3,1.0,405)
gardenbed('Sauna aromatic flower garden',-18.8,4.8,1.3,1.55,406)
gardenbed('Quiet room west native garden',-15.8,12.0,1.0,2.0,407)
gardenbed('Arrival flowering island',2.1,-7.1,2.1,1.05,408)
gardenbed('Food garden pollinator border',29.8,6.7,2.1,.60,409)
gardenbed('Quiet bench flower garden',30,13.9,2.1,.8,410)
# Shrub-scale green foliage with rose bottlebrush-inspired flowers, grouped in selected beds.
for idx,(cx,cy) in enumerate([(9,-7),(12,-7),(11,-5),(16,7.5),(-18.8,4.8),(-15.8,11),(-15.8,13),(29.7,6.8)]):
rng=random.Random(700+idx);g=ground(cx,cy);vs=[];fs=[];mis=[]
for j in range(140):
a=rng.random()*2*pi;b=rng.random()*pi;rr=rng.uniform(.25,.66);x=cx+rr*cos(a)*sin(b);y=cy+rr*sin(a)*sin(b);z=g+.7+rr*cos(b)*.7;d=rng.uniform(.055,.12);k=len(vs)
vs.extend([(x-d*cos(a),y-d*sin(a),z),(x-.035*sin(a),y+.035*cos(a),z+.025),(x+d*cos(a),y+d*sin(a),z+.045),(x+.035*sin(a),y-.035*cos(a),z+.025)]);fs.append((k,k+1,k+2,k+3));mis.append(j%4)
for j in range(12):
a=j*2*pi/12;x=cx+.5*cos(a);y=cy+.5*sin(a);z=g+.9+rng.uniform(-.15,.2)
for n in range(12):
b=n*2*pi/12;k=len(vs);vs.extend([(x,y,z-.07),(x+.04*cos(b),y+.04*sin(b),z),(x,y,z+.08)]);fs.append((k,k+1,k+2));mis.append(4)
ob=mesh('Bottlebrush inspired flowering shrub '+str(idx+1),vs,fs)
for m in leafm+[flowers[2]]:ob.data.materials.append(m)
ob.data.polygons.foreach_set('material_index',mis)
# Refresh affected lightweight geometry, keeping the existing simplified tree canopy.
proxy_map={'33 Daily life - training gathering play and food':'FAST | Training gathering play and garden loop'}
def combine(c,name):
old=bpy.data.objects.get(name)
if old:bpy.data.objects.remove(old,do_unlink=True)
vs=[];fs=[];inds=[];smooths=[];mats=[];mapping={}
for ob in c.objects:
if ob.type!='MESH' or ob.hide_render or ob.hide_get():continue
remap=[]
for m in ob.data.materials:
if m not in mapping:mapping[m]=len(mats);mats.append(m)
remap.append(mapping[m])
mw=matrix(ob)
if 'individual canopy leaves' in ob.name:
for k in range(0,len(ob.data.vertices),20):
c0=sum((ob.data.vertices[k+j].co for j in range(4)),Vector())/4;off=len(vs);vs.extend(tuple(mw@(c0+(ob.data.vertices[k+j].co-c0)*1.55)) for j in range(4));fs.append((off,off+1,off+2,off+3));inds.append(remap[ob.data.polygons[k//5*4].material_index]);smooths.append(False)
continue
off=len(vs);vs.extend(tuple(mw@v.co) for v in ob.data.vertices)
for p in ob.data.polygons:fs.append(tuple(off+i for i in p.vertices));inds.append(remap[p.material_index]);smooths.append(p.use_smooth)
if not fs:return
me=bpy.data.meshes.new(name);me.from_pydata(vs,[],fs);me.update()
for m in mats:me.materials.append(m)
me.polygons.foreach_set('material_index',inds);me.polygons.foreach_set('use_smooth',smooths);ob=bpy.data.objects.new(name,me);fast.objects.link(ob)
for cname in dirty:
c=bpy.data.collections.get(cname)
if c and not cname.startswith('30'):combine(c,proxy_map.get(cname,'FAST | '+cname))
combine(col,'FAST | Aligned garden court and flowers')
# Shift camera chapters with their moved subject; replace play chapter with landscape.
cam=bpy.data.objects['PLAY TOUR camera'];chapters=json.load(open(OUT+'/tour-chapters-daily-life.json'))
def fcurves(owner):
for layer in owner.animation_data.action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
yield from bag.fcurves
for number,delta in [(10,(0,2.2,0)),(11,(0,2.2,0)),(12,(0,2.2,0)),(15,(-3.8,-1.3,0)),(18,(-.8,0,0)),(19,(-.8,0,0)),(20,(3,0,0)),(21,(3,0,0)),(25,(2.8,-1,0))]:
c=chapters[number-1]
for fc in fcurves(cam):
if fc.data_path=='location':
for k in fc.keyframe_points:
if c['start']<=k.co.x<=c['end']:
d=delta[fc.array_index];k.co.y+=d;k.handle_left.y+=d;k.handle_right.y+=d
newshots={1:('01 / ONE CONNECTED GARDEN HOMESTEAD',(44,56,48),(42,54,46),(0,-5,0),37),22:('22 / BALANCED FUTURE PAVILIONS',(23,32,17),(22,30,16),(0,15,1),35),23:('23 / RECTANGLES AND GARDEN CURVES',(0,38,30),(0,36,28),(0,4,0),36),27:('27 / FLOWERING GARDEN - ROOM TO BREATHE',(19,-.5,8),(18,-1.5,7),(10.9,-6.8,.8),32),28:('28 / FLOWING WALKS AND GARDEN COURT',(22,27,17),(20,25,15),(0,13,0),35)}
fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
for number,(title,a,b,target,lens) in newshots.items():
c=chapters[number-1];c['title']=title
for f,p in [(c['start'],a),(c['end'],b)]:
s.frame_set(f);cam.location=p;cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler();cam.keyframe_insert('location',frame=f);cam.keyframe_insert('rotation_euler',frame=f);cam.data.lens=lens;cam.data.keyframe_insert('lens',frame=f)
for ob in fonts:
if ob.data.body.startswith(str(number).zfill(2)+' /'):
ob.data.body=title;half=36/lens;ob.location=(-half*.91,half*9/16*.84,-2)
s.timeline_markers.clear()
for c in chapters:s.timeline_markers.new(c['title'],frame=c['start'])
s['Current design']='Aligned garden homestead: main house and garage share a north-south axis; meditation wing and future east pavilion balance the oval garden. Smooth gravel routes, a curved covered timber breezeway and layered flowering beds replace the isolated table-tennis deck.'
s['Landscape design basis']='Preserve north-facing outlook and open air movement; low planting close to buildings, repeated beds and a simple hierarchy of shared court, pedestrian paths and vehicle arrival. Concept terrain remains unsurveyed.'
s['PLAY THE TOUR']='Hover over the 3D view and press Space. 30 chapters, 2 min 30 sec. Shift+Left Arrow restarts.'
text=bpy.data.texts.get('START HERE - PRESS PLAY')
if text:text.clear();text.write(s['PLAY THE TOUR']+'\n\n'+s['Current design'])
layout={'version':4,'main':[0,0,12,6],'parents':[-11.8,-4.9,8,5],'meditation':[-11.8,12,4.5,4.8],'future':[[11.8,12,4.5,4.8],[0,24,4.5,4.8]],'garage':[0,-19,8,7],'training':[-9.4,-19,4.8,5.4],'solar':[9.4,-19,7.3,5.2],'sauna':[-23,4.2],'outdoor_table':[10,3.5,6,5.8],'court':[0,13.2,8.5,5.3],'paths':paths,'beds':beds,'table_tennis':False,'future_basketball':[13,-36,10,10]}
json.dump(layout,open(OUT+'/garden-flow-layout.json','w'),indent=2);json.dump(chapters,open(OUT+'/tour-chapters-garden-flow.json','w'),indent=2)
report={'deleted_objects':len(removed),'moved_objects':len(moved),'flower_beds':len(beds),'planted_clumps':len(flower_centers),'flowering_shrubs':8,'smooth_routes':len(paths),'table_tennis_removed':not any('table tennis' in o.name.lower() or o.name.startswith('PLAY DECK') for o in s.objects),'future_pavilions':2,'tour_chapters':len(chapters)}
assert report['table_tennis_removed']
json.dump(report,open(OUT+'/garden-flow-checks.json','w'),indent=2)
s.camera=cam;s.frame_set(1);s.render.resolution_percentage=100
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-garden-flow-tour.blend')
for o in fonts:o.hide_render=True
s.render.resolution_percentage=60;s.cycles.samples=32;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'
for filename,frame in [('garden-flow-overview.png',45),('garden-court.png',2670),('flowering-kitchen-garden.png',3160),('garden-flow-training.png',2920),('garden-flow-dining.png',3035)]:
s.frame_set(frame);s.render.filepath=OUT+'/'+filename;bpy.ops.render.render(write_still=True)
print('GARDEN_FLOW_READY '+json.dumps(report),flush=True)