Files
samford-valley-home/source/samford-spacious-tour/children_and_final_tour.py

152 lines
13 KiB
Python

import bpy, os, math, json, bmesh
from mathutils import Vector
from math import sin,cos,pi
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
COL={}
for key,name in [('one','23 FUTURE - Child 1 private room'),('two','24 FUTURE - Child 2 private room'),('three','25 FUTURE - Child 3 private room'),('links','26 Children courtyard and covered links')]:
c=bpy.data.collections.new(name); s.collection.children.link(c); COL[key]=c
active='links'; src=open(os.path.join(OUT,'../samford-modular/build_scene.py')).read(); exec(src[src.index('def mesh('):src.index('timber=textured(')])
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']; roofmat=bpy.data.materials['Pale Colorbond - powder coated steel']; clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; glass=bpy.data.materials['Clear glazing']; weather=bpy.data.materials['Naturally silvered hardwood - horizontal X']; bamboo=bpy.data.materials['Bamboo lining - golden fibres']; linen=bpy.data.materials['Undyed flax linen']; F=1.12
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
for name in ['13 NEXT - optional attached room modules','14 LATER - independent dwelling and workshop']:
c=bpy.data.collections.get(name)
if c:
for o in list(c.objects): bpy.data.objects.remove(o,do_unlink=True)
bpy.data.collections.remove(c)
for o in list(bpy.data.collections['20 Spacious site connections'].objects):
if o.name.startswith(('Optional extension link','Future north dwelling reserved access','Future workshop reserved access')): bpy.data.objects.remove(o,do_unlink=True)
# Repair the old west wall boolean so the short corridor is physically open to the main room.
wall=bpy.data.objects['Insulated end wall']; bm=bmesh.new(); bm.from_mesh(wall.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(wall.data); bm.free()
cut=box('TEMP correct central-room doorway',(-6,-1.8,F+1.16),(.85,1.13,2.34),None)
mod=wall.modifiers.new('Open doorway into short bedroom corridor','BOOLEAN'); mod.operation='DIFFERENCE'; mod.solver='EXACT'; mod.object=cut; bpy.context.view_layer.objects.active=wall; bpy.ops.object.modifier_apply(modifier=mod.name); bpy.data.objects.remove(cut,do_unlink=True)
accents=[mat('Child 1 example clay accent',(.40,.20,.12),.85),mat('Child 2 example sage accent',(.25,.37,.26),.85),mat('Child 3 example blue accent',(.20,.31,.39),.85)]
cabins=[('one',-11,12),('two',-3,17),('three',7,15)]
for idx,(key,cx,cy) in enumerate(cabins):
active=key; w=4.5; dep=4.8; xs=cx-w/2; xe=cx+w/2; ys=cy-dep/2; yn=cy+dep/2; a=accents[idx]
box('Child %d | timber insulated floor'%(idx+1),(cx,cy,F-.11),(w,dep,.22),wood,.012)
for x in [xs+.20,xe-.20]:
for y in [ys+.20,yn-.20]:
g=ground(x,y); box('Child room stone pier',(x,y,(g+F-.22)/2),(.28,.28,F-.22-g),stone,.01)
box('Child room steel post shoe',(x,y,F-.23),(.18,.18,.09),steel,.005)
# South entry facade has a solid door and a shaded sidelight.
for x,ww in [(cx-1.42,1.66),(cx+1.42,1.66)]: box('Child room south clay wall',(x,ys,F+1.35),(ww,.17,2.7),clay)
box('Child room south door header',(cx,ys,F+2.49),(1.2,.17,.42),clay)
box('Child %d | independent front door'%(idx+1),(cx,ys-.05,F+1.14),(1.04,.05,2.28),wood,.009)
for x in [cx-.56,cx+.56]: box('Child room entry timber jamb',(x,ys-.07,F+1.17),(.07,.12,2.34),woodz,.005)
rod('Child room door handle',(cx+.39,ys-.1,F+1.1),(cx+.25,ys-.1,F+1.1),.015,steel)
# North wall window and east window give two sides for operable ventilation.
for x,ww in [(cx-1.7,1.10),(cx+1.7,1.10)]: box('Child room north clay wall',(x,yn,F+1.5),(ww,.17,3.0),clay)
for z,hh in [(F+.45,.9),(F+2.75,.50)]: box('Child room north window infill',(cx,yn,z),(2.32,.17,hh),clay)
box('Child room north openable glazing',(cx,yn,F+1.7),(2.24,.02,1.55),glass)
for x in [cx-1.16,cx+1.16]: box('Child room north window frame',(x,yn,F+1.7),(.055,.12,1.64),woodz)
for z in [F+.88,F+2.52]: box('Child room north window rail',(cx,yn,z),(2.36,.12,.06),wood)
box('Child room west clay wall',(xs,cy,F+1.43),(.17,dep,2.86),clay)
for y,dd in [(cy-1.68,1.44),(cy+1.68,1.44)]: box('Child room east clay wall',(xe,y,F+1.43),(.17,dd,2.86),clay)
for z,hh in [(F+.5,1),(F+2.59,.54)]: box('Child room east window infill',(xe,cy,z),(.17,1.92,hh),clay)
box('Child room east openable glazing',(xe,cy,F+1.66),(.018,1.90,1.3),glass)
# Exterior timber weatherboards, kept outside the window apertures.
for j in range(18):
z=F+.075+j*.15
for x,ww in [(cx-1.44,1.57),(cx+1.44,1.57)]: box('Child room front weatherboard',(x,ys-.11,z),(ww,.045,.142),weather)
box('Child room west weatherboard',(xs-.11,cy,z),(.045,dep,.142),weather)
for x in [xs,xe]:
for y,h in [(ys,2.75),(yn,3.1)]: box('Child room exposed corner post',(x,y,F+h/2),(.10,.10,h),woodz,.005)
for i in range(8):
x=xs+i*w/7; beam('Child room exposed roof rafter',(x,ys-.3,F+2.73),(x,yn+.3,F+3.13),.055,.15,wood)
for i in range(33):
x=xs+(i+.5)*w/33; beam('Child room bamboo ceiling',(x,ys,F+2.85),(x,yn,F+3.20),w/33-.004,.02,bamboo)
ro=box('Child room simple mono-pitch roof',(cx,cy,F+3.03),(w+.65,dep+.65,.07),roofmat,.008); ro.rotation_euler.x=math.atan(.35/dep)
box('Child room covered entry deck',(cx,ys-.65,F-.035),(2.2,1.3,.07),wood,.009)
canopy=box('Child room entry awning',(cx,ys-.65,F+2.55),(2.4,1.5,.065),roofmat,.01)
# A complete private room with freely replaceable decor.
bx=cx-1.20; by=cy+.25
box('Child %d | single bed base'%(idx+1),(bx,by,F+.21),(1.05,2.10,.36),wood,.02)
box('Child room mattress',(bx,by,F+.45),(1.0,2.02,.19),linen,.06)
box('Child room sample coloured blanket',(bx,by-.30,F+.555),(1.01,1.18,.035),a,.025)
box('Child room pillow',(bx,by+.72,F+.60),(.70,.40,.12),linen,.06)
box('Child room desk',(cx+.90,cy+.82,F+.75),(1.58,.62,.055),wood,.01)
for x in [cx+.22,cx+1.58]: box('Child room desk leg',(x,cy+.82,F+.37),(.05,.51,.74),steel)
box('Child room desk stool',(cx+.9,cy+.12,F+.44),(.40,.40,.055),wood,.01)
for x in [cx+.74,cx+1.06]:
for y in [cy-.04,cy+.28]: box('Child room stool leg',(x,y,F+.21),(.03,.03,.42),wood)
box('Child room wardrobe',(cx+1.60,cy-1.54,F+1.0),(.66,.72,2.0),wood,.012)
box('Child room personal pinboard',(xs+.10,cy-.15,F+1.75),(.035,1.35,.77),a,.01)
for j in range(3): box('Child room replaceable display shelf',(cx+.90,yn-.20,F+1.35+j*.37),(1.6,.28,.025),wood)
cu=bpy.data.curves.new('Child room sign','FONT'); cu.body='CHILD %d / YOUR SPACE'%(idx+1); cu.size=.085; cu.align_x='CENTER'; ob=bpy.data.objects.new(cu.body,cu); COL[active].objects.link(ob); ob.location=(cx,ys-.145,F+2.4); ob.rotation_euler=(pi/2,0,0); cu.materials.append(steel)
ld=bpy.data.lights.new('Child room warm ceiling light','AREA'); ld.energy=120; ld.size=2; ld.color=(1,.86,.7); lo=bpy.data.objects.new(ld.name,ld); COL[active].objects.link(lo); lo.location=(cx,cy,F+2.6)
COL[active]['Purpose']='Future private bedroom for one child, not a separate self-contained dwelling. Shared kitchen/dining and bathroom in the family home. Decor shown is replaceable example only.'
active='links'
for cx,cy,w,dep in [(-3,10,1.2,9.2),(-7,9,8,1.2),(2,9,10,1.2),(7,10.8,1.2,3.6)]:
box('Children shared covered walk deck',(cx,cy,F-.07),(w,dep,.14),wood,.008)
box('Children shared covered walk roof',(cx,cy,F+2.55),(w+.14,dep+.14,.055),roofmat,.008)
n=max(2,int(max(w,dep)/2.7)+1)
for j in range(n):
for side in [-1,1]:
x=cx-w/2+.10+j*(w-.20)/(n-1) if w>dep else cx+side*(w/2-.08)
y=cy+side*(dep/2-.08) if w>dep else cy-dep/2+.10+j*(dep-.20)/(n-1)
g=ground(x,y); box('Children walkway post',(x,y,(g+F+2.52)/2),(.075,.075,F+2.52-g),woodz,.004)
# Courtyard-facing directions keep the children connected to the family common room.
cam=bpy.data.objects['PLAY TOUR camera']; data=cam.data
newshots={
17:('18 / CHILD 1 - THEIR OWN ROOM',(-10.15,10.30,2.9),(-10.35,10.5,2.8),(-11.45,12.5,1.8),(-11.5,12.7,1.8),24),
18:('19 / CHILD 2 - THEIR OWN ROOM',(-2.15,15.30,2.9),(-2.35,15.5,2.8),(-3.45,17.5,1.8),(-3.5,17.7,1.8),24),
19:('20 / CHILD 3 - THEIR OWN ROOM',(7.85,13.30,2.9),(7.65,13.5,2.8),(6.55,15.5,1.8),(6.5,15.7,1.8),24),
20:('21 / THREE PRIVATE ROOMS - ONE FAMILY',(18,-1,14),(15,2,12),(-2,13,2),(-2,13,2),37),
21:('22 / LIVE FIRST. GROW TOGETHER.',(44,64,47),(51,71,51),(-4,0,1),(-4,0,1),40),
}
tour=bpy.data.collections['22 PLAY - guided camera tour']; caption=bpy.data.materials['Tour captions ivory']
for o in list(tour.objects):
if o.type=='FONT' and int(o.name[:2])>=18: bpy.data.objects.remove(o,do_unlink=True)
for marker in list(s.timeline_markers):
if marker.frame>=2041: s.timeline_markers.remove(marker)
chapters=json.load(open(os.path.join(OUT,'tour-chapters.json')))[:17]
for i,(title,a,b,ta,tb,lens) in newshots.items():
start=i*120+1; end=(i+1)*120
for frame,pos,target in [(start,a,ta),(end,b,tb)]:
cam.location=pos; cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=frame); cam.keyframe_insert('rotation_euler',frame=frame); data.lens=lens; data.keyframe_insert('lens',frame=frame)
s.timeline_markers.new(title,frame=start)
cu=bpy.data.curves.new(title,'FONT'); cu.body=title; cu.size=.036; ob=bpy.data.objects.new(title,cu); tour.objects.link(ob); ob.parent=cam; half=36/lens; ob.location=(-half*.91,half*9/16*.84,-2); ob.show_in_front=True; cu.materials.append(caption)
for f,val in [(0,0),(start-1,0),(start,1),(end,1),(end+1,0)]: ob.scale=(val,)*3; ob.keyframe_insert('scale',frame=f)
chapters.append({'title':title,'start':start,'end':end,'seconds':5})
def curves(action):
if hasattr(action,'fcurves'): return action.fcurves
result=[]
for layer in action.layers:
for strip in layer.strips:
for slot in action.slots:
bag=strip.channelbag(slot)
if bag: result.extend(bag.fcurves)
return result
for owner in [cam,data]:
for fc in curves(owner.animation_data.action):
for k in fc.keyframe_points: k.interpolation='LINEAR'
for o in tour.objects:
if o.type=='FONT':
for fc in curves(o.animation_data.action):
for k in fc.keyframe_points: k.interpolation='CONSTANT'
# Correct normals on newly made solids; outline transparent glass in fast viewport mode.
for c in COL.values():
for o in c.objects:
if o.type=='MESH':
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()
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.display_type='WIRE'
s.frame_end=2640; s.frame_set(1); s.camera=cam; s.sync_mode='FRAME_DROP'
s['PLAY THE TOUR']='Press Space over the 3D viewport or click Timeline Play. Shift+Left Arrow returns to frame 1. 110 seconds, 22 chapters, 24 fps. Saved in camera view.'
s['Future additions']='Exactly three separate children bedroom cabins, one for each child. 4.5m x 4.8m footprints, private doors, bed, desk, wardrobe and personal display area. Covered walks connect them to the shared home. No extra kitchens/bathrooms in the cabins.'
s['Land planning']='Concept can be investigated on roughly 1-2 hectares of usable land; 2 hectares is a comfortable search target, not a site-specific sufficiency or planning guarantee. No cadastral boundaries or survey supplied.'
txt=bpy.data.texts['START HERE - PRESS PLAY']; txt.clear(); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Spacious layout']+'\n\n'+s['Future additions']+'\n\n'+s['Land planning']+'\n\n'+s['Viewport performance'])
json.dump(chapters,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
report=json.load(open(os.path.join(OUT,'tour-checks.json'))); report.update({'duration_seconds':110,'frames':2640,'shots':22,'future_child_rooms':3,'child_room_footprints':[4.5,4.8],'generic_future_office_guest_workshop_removed':True}); json.dump(report,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
s.render.engine='CYCLES'; s.render.resolution_percentage=100
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
# All final chapter thumbnails, plus motion endpoints for the corridor and children rooms.
for o in s.objects:
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.hide_render=True
if o.name=='Subtle atmosphere over hillside': o.hide_render=True
if o.type=='MESH' and o.name.startswith(('Native grasses','Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): o.hide_render=True
s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.light='STUDIO'; s.display.shading.color_type='MATERIAL'; s.display.shading.show_shadows=True; s.display.shading.show_cavity=True
for i in range(22):
s.frame_set(i*120+60); s.render.filepath=os.path.join(OUT,'check-%02d.png'%(i+1)); bpy.ops.render.render(write_still=True)
print('FAMILY_TOUR_FINAL_REVIEW_READY',flush=True)