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
|
||||
}
|
||||
36
source/samford-retreat-garage/README.md
Normal file
36
source/samford-retreat-garage/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Samford solarpunk homestead — smooth tour
|
||||
|
||||
Open **samford-solarpunk-smooth-tour.blend**. It is saved at frame 1 in the animated camera view. Hover over the 3D view and press **Space**, or click the Timeline Play triangle. Space pauses. **Shift + Left Arrow** returns to the start. The tour runs for **115 seconds across 23 chapters**.
|
||||
|
||||
## Smoother playback and solarpunk update
|
||||
|
||||
The interactive scene uses simplified foliage and merged static display meshes: 47 visible objects and approximately 198,000 faces, down from 4,090 objects and 1.91 million faces. Matching Workbench preview tests were approximately 6–7 times faster after warm-up; these timings are not a measurement of interactive FPS. Solid shading is selected, with expensive cavity and shadow effects disabled.
|
||||
|
||||
Full editable geometry and PBR materials remain under **DETAIL - full geometry for renders**. This collection is disabled only in the viewport. Cycles renders automatically use the detailed scene and exclude **00 FAST TOUR - lightweight viewport**. For detailed editing, expose the monitor column in the Outliner filter menu, enable DETAIL and disable FAST TOUR; reverse those two switches for playback. The lightweight meshes are static display copies, so edit the originals for final renders.
|
||||
|
||||
The garage now contains a simplified pearl-white Tesla Model Y with panoramic glazing, Tesla badge, aero wheels and a cable connected to a wall charger. Eight additional garage roof solar modules and a planted timber trellis extend the solarpunk theme. The vehicle is a concept representation based on the [2020–2024 Tesla Model Y reference](https://www.tesla.com/ownersmanual/2020_2024_modely/en_cn/Owners_Manual.pdf). Solar charging is a visual concept, with no electrical capacity simulation.
|
||||
|
||||
`tesla-solarpunk-garage.png` shows the updated garage. `performance-checks.json` records performance and model verification.
|
||||
|
||||
## What changed
|
||||
|
||||
- Two of the three solid children’s cabins are removed, including their furniture and unnecessary covered walks. Their locations are now faint wire outlines and translucent ground shadows, labelled as unbuilt future spaces in the tour.
|
||||
- The remaining cabin is a meditation room with a woven mat, square zabuton, one round cushion, a low stone candle shelf with three candles, and a simple timber bench. The former bed, desk, wardrobe and decoration are removed.
|
||||
- A 4m-wide gravel driveway approaches from the southeast and reaches a gravel turning apron outside a detached 8 × 7m garage. The garage contains the Tesla, a timber workstation with tools and a small display, and two bicycles in a three-position rack.
|
||||
- The living pavilion, parents’ wing, corridor, meditation room, sauna, greenhouse and garage have transparent presentation facades. Timber posts and frames remain visible.
|
||||
- Kitchen/dining, parents’ bedroom and bathroom, six dining seats, homelab, solar, two rainwater tanks, greenhouse and garden beds remain.
|
||||
|
||||
## Transparent viewing sides
|
||||
|
||||
Collection **30 VIEW - Transparent building sides** contains the retained wall skins. They use near-transparent materials in Cycles and are hidden as filled surfaces in the fast solid viewport. These are visual cutaways, not a specification to build open or transparent exterior walls. Each wall object records its original material names for restoration. The rest of the building remains editable.
|
||||
|
||||
## Files
|
||||
|
||||
- `retreat-garage-overview.png` — road, garage and homestead.
|
||||
- `meditation-room.png` — cushion, candles and simple interior.
|
||||
- `garage-workstation-bikes.png` — parked car, workbench and bikes.
|
||||
- `retreat-garage-plan.svg` — updated labelled layout.
|
||||
- `design-checks.json` — scene counts and tour verification.
|
||||
- `tour-chapters.json` — chapter frames and titles.
|
||||
|
||||
The earlier family-cabin version is preserved in the adjacent samford-spacious-tour folder. This is an architectural scene, not a surveyed site or construction design. Road access and gradients, drainage, garage structure, ventilation, fire and final equipment requirements still depend on the actual property and detailed design.
|
||||
117
source/samford-retreat-garage/UNREAL-PROJECT-SCOPE.md
Normal file
117
source/samford-retreat-garage/UNREAL-PROJECT-SCOPE.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Samford Solarpunk Homestead — Unreal Engine 5 project scope
|
||||
|
||||
Prepared 10 September 2026. Proposal only; no Unreal installation, import or project build has been performed.
|
||||
|
||||
## Recommended outcome
|
||||
|
||||
An editable UE5 architectural scene and a local Mac application for exploring the current homestead. Keep Blender as the source for building design; use Unreal for real-time presentation. The first release preserves the current layout, natural material palette, solar infrastructure and future expansion footprints.
|
||||
|
||||
Assumptions: one local user, keyboard/mouse or trackpad, this Mac as the first target, fixed golden-hour lighting, and no construction-grade or energy simulation. The scope reuses the existing concept models; a commercial-quality replacement vehicle or furniture library is additional art work.
|
||||
|
||||
## Existing source and local readiness
|
||||
|
||||
- Source: `samford-solarpunk-smooth-tour.blend` in this directory. Its hidden DETAIL collection contains the complete render geometry. Its FAST TOUR collection is a separate, simplified viewport copy. Exporting both would duplicate the scene.
|
||||
- Existing assets: living/kitchen/dining pavilion; parents' bedroom and bathroom wing; connecting corridor and verandahs; meditation room; two future shadow footprints; sauna and shower; greenhouse and six beds; tanks; roof and ground solar; Starlink and homelab; driveway; garage, Tesla concept car, charging point, workbench and bikes.
|
||||
- Existing tour: 115 seconds, 23 chapters, animated camera and lens, chapter labels and deliberate cuts.
|
||||
- Measured Blender display: approximately 4,090 visible objects / 1.91 million faces before optimization, versus 47 objects / 198,000 faces in the fast version. These are Blender polygon counts, not Unreal triangle counts or Unreal performance measurements.
|
||||
- Local hardware: Apple M5, 24 GiB unified memory. macOS 26.6.2; Xcode 26.6. Approximately 194 GiB free on the workspace volume at inspection.
|
||||
- No UnrealEditor application was found by Spotlight; no Epic Games installation was present in the standard shared folder or Applications listing. An installation elsewhere remains possible.
|
||||
- Epic recommends 32 GB or more memory. The current Mac is above the listed minimum but below that recommendation. The pilot must establish a practical visual-quality target on this machine. [Epic Mac requirements](https://dev.epicgames.com/documentation/en-us/unreal-engine/macos-development-requirements-for-unreal-engine)
|
||||
|
||||
## Approaches considered
|
||||
|
||||
| Approach | Estimated effort | Result |
|
||||
|---|---:|---|
|
||||
| Basic conversion | 12–24 hours | Imported geometry, basic materials, simple fly view. Useful proof, limited polish and interaction. |
|
||||
| Optimized interactive homestead — recommended | 48–80 hours total | Walk/fly controls, complete tour, cutaways, future footprints, rebuilt materials and vegetation, tested local Mac application. |
|
||||
| Showcase visualization | 100–180 hours total | Recommended version plus substantial model upgrades, richer botanical assets, more elaborate cinematics and atmosphere. |
|
||||
|
||||
These are planning estimates for one experienced Unreal architectural-visualization generalist, not measured execution times or a fixed quote. Installation downloads and first shader compilation add elapsed time. The options are alternative totals, not additive phases.
|
||||
|
||||
## First release: user experience
|
||||
|
||||
1. A simple start screen: **Walk**, **Fly**, **Take the tour**, **Quality**, **Quit**.
|
||||
2. Walk at human scale through the home, meditation cabin, garage, sauna and garden. Floors, stairs/ramps and door openings must work physically; presentation-only wall transparency is not sufficient.
|
||||
3. Fly freely to understand the terrain, solar arrays, road and spatial relationships.
|
||||
4. Play the 23-chapter tour with pause/resume, restart and chapter selection. Recreate captions as screen UI rather than 3D camera-attached text.
|
||||
5. In fly/exploration view, **See inside** toggles the presentation wall sections. Walk mode uses coherent physical boundaries and accessible doors.
|
||||
6. **Future spaces** shows/hides the two existing ghost footprints and their labels. Fully furnished future buildings are outside this first release.
|
||||
7. **Balanced** and **Fast** presets adjust foliage, lighting, shadows and rendering resolution. Keep the interface small and nontechnical.
|
||||
|
||||
The Tesla, solar arrays, server, tanks and sauna are visual assets. Driving, charging simulation, water balance, food production and heat simulation are not included.
|
||||
|
||||
## Conversion and scene design
|
||||
|
||||
### Asset preparation
|
||||
|
||||
Work from a duplicate of the detailed source. Create stable export names and pivots; apply necessary modifiers; check normals, openings, thin surfaces and object scale. Validate that Blender metres become the correct Unreal centimetres and that camera orientation, sun direction and site north remain consistent.
|
||||
|
||||
Use FBX mesh exports grouped by building and asset family, with an explicit placement manifest for repeatable imports. Run a small glTF comparison only if FBX material or hierarchy import is troublesome. Keep doors, removable presentation walls, roofs, floors and furniture as logical separate pieces. Merge minor static parts locally and instance repeated posts, panels and vegetation. Avoid importing thousands of unrelated tiny Actors, or merging the entire property into one mesh.
|
||||
|
||||
FBX scene import supports geometry and basic materials, but it does not guarantee the source appearance or animated-camera import. This is a conversion workflow, not a one-click format change. [Epic FBX scene import](https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-scene-import-in-unreal-engine)
|
||||
|
||||
### Materials and lighting
|
||||
|
||||
Create UVs where needed. Bake or rebuild the Blender procedural materials: timber grain, clay, concrete/stone, bamboo, metals, solar glass, glazing, vegetation and upholstery. Start with reusable 1K–2K texture sets and use higher resolution only where the close-up view requires it. Check texture colour spaces, normal-map orientation and transparency.
|
||||
|
||||
Rebuild the warm morning lighting and restrained haze in Unreal. Test software Lumen first, with a simpler fixed-lighting alternative if necessary. Epic currently lists software Lumen on Apple Silicon, hardware-ray-traced Lumen/MegaLights as unsupported on Mac, and Nanite/Virtual Shadow Maps on M2+ as beta support. Nanite will not be a baseline dependency. [Epic rendering feature support](https://dev.epicgames.com/documentation/en-us/unreal-engine/macos-development-requirements-for-unreal-engine)
|
||||
|
||||
### Terrain and vegetation
|
||||
|
||||
Retain the existing terrain shape for the first release; introduce a native Unreal Landscape only if sculpting or foliage workflow materially benefits. Convert trees, groundcover and garden plants into reusable meshes with distance-based detail and culling. Preserve the open views and subtropical character. Use instanced static-mesh foliage; large amounts of individual Actor foliage would repeat the current scene's object-overhead problem. [Epic foliage workflow](https://dev.epicgames.com/documentation/en-us/unreal-engine/foliage-mode-in-unreal-engine)
|
||||
|
||||
### Tour, interaction and updates
|
||||
|
||||
Build one Blueprint-based local experience and a Sequencer tour. Transfer sampled camera transforms and lens values, then verify axes, cuts, interpolation and all chapter boundaries. FBX camera animation needs a dedicated Sequencer workflow or a sampled-data importer; it should not be assumed to arrive with the static scene. [Epic cinematic animation import](https://dev.epicgames.com/documentation/unreal-engine/import-and-export-cinematic-fbx-animations-in-unreal-engine)
|
||||
|
||||
Use separate groups for present buildings, future outlines, cutaway faces, landscape and services. Keep interaction logic separate from imported geometry so subsequent Blender reimports do not erase it. A full live synchronization system is not included.
|
||||
|
||||
## Work packages and effort
|
||||
|
||||
| Stage | Work and checkpoint | Hours |
|
||||
|---|---|---:|
|
||||
| 1. Compatibility and pilot | Confirm engine/toolchain; import one furnished room, glass, one tree and a short camera move; package and benchmark on this Mac. Choose rendering approach. | 4–8 |
|
||||
| 2. Geometry conversion | Clean and export the whole site; organize assets, pivots and instances; repair openings and build simple collision. | 8–12 |
|
||||
| 3. Materials | Bake/rebuild the material palette; validate indoor and outdoor appearance and texture memory. | 10–16 |
|
||||
| 4. Landscape and lighting | Optimize vegetation, terrain, shadows, warm lighting and haze. | 10–16 |
|
||||
| 5. Interaction and tour | Walking/flying, doors as needed, 23 chapters, simple menu, future and cutaway toggles. | 10–16 |
|
||||
| 6. Performance and delivery | Profile packaged build, tune quality presets, test the entire tour and deliver editable project and local app. | 6–12 |
|
||||
| **Total** | **Approximately 6–10 eight-hour working days** | **48–80** |
|
||||
|
||||
The pilot is included in the total. If it cannot meet a usable baseline on this Mac, resolve the lighting/foliage approach before converting the remaining assets. Installation/toolchain problems or substantial remodeling can move the estimate outside this range.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Local Mac application launches without the Unreal Editor open; editable project reopens successfully.
|
||||
- All listed current structures and contents are present at the correct scale, with no duplicate FAST/DETAIL export.
|
||||
- All intended rooms and outdoor paths are accessible in walk mode; no falling through floors or invisible blocking presentation walls.
|
||||
- All 23 chapters play in order, pause/resume/restart work, and each chapter shows its intended subject without unintended clipping.
|
||||
- Future and cutaway toggles work; reopening the application returns to a sensible starting state.
|
||||
- No missing textures or materially incorrect glass, black surfaces or severe light leaks on the agreed inspection route.
|
||||
- Initial performance target: 30 FPS at 1920×1080 output in Balanced mode, using upscaling if needed. Aim for 60 FPS in Fast mode. Both are targets, not promises before the pilot.
|
||||
- Benchmark the packaged application after shader warm-up over a repeatable exterior/interior route; record average and slow-frame timings, quality settings and memory behavior. Complete a ten-minute session without crashes or sustained memory pressure. Profile editor performance separately.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- Editable Unreal project with organized assets and an asset/source map.
|
||||
- Local Mac application, tested on this machine.
|
||||
- Preserved Blender original and a documented export/reimport workflow.
|
||||
- 23-chapter tour, room navigation and the agreed toggles.
|
||||
- Performance results and a short illustrated controls guide.
|
||||
- Optional recorded walkthrough video; final video resolution and quality can be set independently of real-time playback.
|
||||
|
||||
## Budget, dependencies and exclusions
|
||||
|
||||
Engine fees may be zero under Epic's individual/small-business terms. Commercial use must be checked against the applicable revenue and licence rules. Optional licensed plants/material assets: allow A$0–500 initially; no purchases are assumed. [Epic licensing](https://www.unrealengine.com/license)
|
||||
|
||||
For comparison only, hiring 48–80 hours at an assumed A$100–160/hour implies approximately A$4,800–12,800 in labour before GST where applicable. The hourly rates are budgeting assumptions, not researched vendor quotes. This is separate from the cost of constructing the real house and from any Codex subscription or usage charges.
|
||||
|
||||
Allow approximately 100–150 GB of working storage for the chosen engine, project, imports and caches, subject to installer options and measured cache growth. Avoid a source-engine build or large unneeded content libraries for this first release.
|
||||
|
||||
The installed Xcode is 26.6. Epic currently lists 26.1.1 as recommended for UE 5.8 and specifically excludes 26.4; this does not establish whether 26.6 works. Verify a compatible engine/toolchain pair in the pilot. Do not globally replace the user's developer-tool configuration as a default setup step.
|
||||
|
||||
Additional scope: Windows packaging/testing; public distribution signing/notarization; VR; browser streaming and hosting; multiplayer; a driveable Tesla; realistic weather/time-of-day controls; fully built future dwellings; a material configurator; photogrammetry/site survey alignment; substantial remodels; architectural compliance; energy, water or construction-cost simulation. The current terrain is a concept landscape, not a surveyed Samford property.
|
||||
|
||||
## Recommended next milestone
|
||||
|
||||
A 4–8-hour compatibility and visual pilot using the living pavilion, a tree, key materials and one camera shot. Its output is a runnable Mac sample, measured frame times, confirmed toolchain and a recommendation for the final visual settings. That makes the full conversion estimate concrete before committing to the remaining work.
|
||||
218
source/samford-retreat-garage/add_daily_life.py
Normal file
218
source/samford-retreat-garage/add_daily_life.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import bpy,math,os,json
|
||||
from mathutils import Vector,Matrix
|
||||
from math import sin,cos,pi
|
||||
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene
|
||||
detail=bpy.data.collections['DETAIL - full geometry for renders']
|
||||
col=bpy.data.collections.new('33 Daily life - training gathering play and food');detail.children.link(col)
|
||||
COL={'life':col};active='life'
|
||||
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'];linen=bpy.data.materials['Undyed flax linen'];gravel=bpy.data.materials['Loose local gravel - fine stones'];F=1.12
|
||||
rubber=mat('Exercise mat muted charcoal',(.035,.048,.044),.94)
|
||||
sage=mat('Table tennis muted green',(.10,.21,.16),.63)
|
||||
cream=mat('Ceramic tea bowls warm white',(.73,.68,.56),.48)
|
||||
white=mat('Table tennis painted court lines',(.8,.79,.71),.63)
|
||||
black=mat('Freezer seals and equipment grips',(.028,.03,.029),.7)
|
||||
metal=mat('Appliance brushed metal',(.44,.46,.43),.36,.7)
|
||||
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
|
||||
def path(n,pts,w):
|
||||
vs=[];fs=[]
|
||||
for a,b in zip(pts,pts[1:]):
|
||||
a,b=Vector(a),Vector(b);delta=b-a;perp=Vector((-delta.y,delta.x)).normalized()*w/2;steps=max(2,int(delta.length/.3));off=len(vs)
|
||||
for i in range(steps+1):
|
||||
q=a+delta*i/steps
|
||||
for v in [q-perp,q+perp]:vs.append((v.x,v.y,ground(v.x,v.y)+.085))
|
||||
for i in range(steps):k=off+2*i;fs.append((k,k+1,k+3,k+2))
|
||||
return mesh(n,vs,fs,gravel)
|
||||
def deck(n,cx,cy,w,d,z):
|
||||
box(n+' accessible pier deck',(cx,cy,z-.09),(w,d,.18),wood,.01)
|
||||
for x in [cx-w/2+.15,cx+w/2-.15]:
|
||||
for y in [cy-d/2+.15,cy+d/2-.15]:
|
||||
g=ground(x,y);box(n+' stone pier',(x,y,(g+z-.18)/2),(.3,.3,max(.08,z-.18-g)),stone,.01)
|
||||
for i in range(int(d/.15)):
|
||||
box(n+' deck board joint',(cx,cy-d/2+.075+i*.15,z+.002),(w,.006,.003),steel)
|
||||
def bench(n,x,y,z,length=2.7):
|
||||
box(n+' seat',(x,y,z+.45),(length,.43,.065),wood,.015)
|
||||
box(n+' linen seat pad',(x,y,z+.502),(length-.1,.39,.04),linen,.016)
|
||||
for xx in [x-length*.37,x+length*.37]:box(n+' leg',(xx,y,z+.21),(.07,.35,.42),woodz,.008)
|
||||
box(n+' backrest',(x,y+.21,z+.75),(length,.055,.27),wood,.012)
|
||||
def ring(n,c,r,t,material):
|
||||
cx,cy,cz=c;vs=[];fs=[];N=48;M=10
|
||||
for i in range(N):
|
||||
a=i*2*pi/N
|
||||
for j in range(M):
|
||||
b=j*2*pi/M;rr=r+t*cos(b);vs.append((cx+rr*cos(a),cy+t*sin(b),cz+rr*sin(a)))
|
||||
for i in range(N):
|
||||
for j in range(M):fs.append((i*M+j,((i+1)%N)*M+j,((i+1)%N)*M+(j+1)%M,i*M+(j+1)%M))
|
||||
ob=mesh(n,vs,fs,material)
|
||||
for p in ob.data.polygons:p.use_smooth=True
|
||||
return ob
|
||||
# Covered exercise pavilion west of the garage. Independent steel exercise frame.
|
||||
tx,ty,tz=-12.2,-18,.95
|
||||
deck('TRAINING',tx,ty,4.8,5.4,tz)
|
||||
for x in [tx-2.28,tx+2.28]:
|
||||
for y,h in [(ty-2.57,2.95),(ty+2.57,3.25)]:
|
||||
box('Training timber canopy post',(x,y,tz+h/2),(.125,.125,h),woodz,.005)
|
||||
box('Training canopy steel shoe',(x,y,tz+.07),(.16,.16,.14),steel,.005)
|
||||
for x in [tx-2.28+i*.57 for i in range(9)]:beam('Training exposed roof rafter',(x,ty-2.9,tz+2.95),(x,ty+2.9,tz+3.29),.065,.18,wood)
|
||||
ro=box('Training pale mono pitch canopy',(tx,ty,tz+3.16),(5.3,6,.065),roof,.006);ro.rotation_euler.x=math.atan(.3/5.4)
|
||||
for x in [tx-.98,tx+.98]:
|
||||
box('EXERCISE FRAME - steel upright',(x,ty+.85,tz+1.4),(.095,.095,2.8),steel,.006)
|
||||
box('Exercise frame bolted foot plate',(x,ty+.85,tz+.02),(.30,.36,.04),steel,.005)
|
||||
for dx in [-.1,.1]:
|
||||
for dy in [-.12,.12]:rod('Exercise frame anchor concept',(x+dx,ty+.85+dy,tz+.04),(x+dx,ty+.85+dy,tz+.067),.011,metal,sides=8)
|
||||
bar=rod('PULL UP BAR - dedicated steel frame',(tx-1.02,ty+.85,tz+2.70),(tx+1.02,ty+.85,tz+2.70),.017,steel,sides=20)
|
||||
bar['Design intent']='Dedicated exercise frame intended to be load rated by its manufacturer or structural engineer. These concept anchor plates do not establish a safe working load.'
|
||||
for x in [tx-.28,tx+.28]:
|
||||
box('Adjustable ring hanging strap',(x,ty+.85,tz+2.10),(.027,.009,1.18),black,.002)
|
||||
ring('WOODEN GYMNASTIC RING',(x,ty+.85,tz+1.40),.125,.017,wood)
|
||||
for x,y in [(tx-.65,ty-.5),(tx+.55,ty-.5)]:box('MOVABLE EXERCISE MAT',(x,y,tz+.022),(.85,1.85,.045),rubber,.025)
|
||||
box('Adjustable weight bench seat',(tx+1.55,ty-.88,tz+.45),(.40,.38,.09),rubber,.035)
|
||||
back=box('ADJUSTABLE BENCH - inclined back',(tx+1.55,ty-.25,tz+.60),(.40,.91,.09),rubber,.035);back.rotation_euler.x=.3
|
||||
beam('Bench adjustable steel spine',(tx+1.55,ty-1.05,tz+.15),(tx+1.55,ty+.23,tz+.41),.065,.065,steel)
|
||||
for y in [ty-1,ty+.15]:box('Bench stabilising foot',(tx+1.55,y,tz+.065),(.59,.065,.065),steel,.009)
|
||||
box('Dumbbell timber stand',(tx-1.55,ty-.5,tz+.72),(.68,1.25,.06),wood,.012)
|
||||
for y in [ty-.95,ty-.05]:
|
||||
box('Dumbbell stand leg',(tx-1.55,y,tz+.34),(.5,.06,.68),steel)
|
||||
for y in [ty-.80,ty-.22]:
|
||||
rod('ADJUSTABLE DUMBBELL grip',(tx-1.73,y,tz+.85),(tx-1.37,y,tz+.85),.019,steel,sides=16)
|
||||
for x in [tx-1.77,tx-1.33]:
|
||||
for dx in [-.06,-.02,.02,.06]:box('Dumbbell selectable weight plate',(x+dx,y,tz+.85),(.027,.22,.23),black,.008)
|
||||
for i,x in enumerate([-9.60,-9.25,-8.90]):
|
||||
top=.76-.19*i;box('Training access step',(x,-17,top-.08),(.36,1.2,.16),wood,.009)
|
||||
path('Training to garage pedestrian link',[(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],1.1)
|
||||
# Outdoor dining: a separate terrace keeps the original narrow veranda clear.
|
||||
dx,dy=10,3.5;deck('GATHERING TERRACE',dx,dy,6,5.8,F)
|
||||
deck('Veranda terrace connection',6.5,4.2,1,1.5,F)
|
||||
box('OUTDOOR TABLE - ten place timber top',(dx,dy,F+.76),(3.2,1.03,.085),wood,.025)
|
||||
for x in [dx-1.13,dx+1.13]:box('Outdoor table trestle',(x,dy,F+.36),(.10,.78,.72),woodz,.01)
|
||||
bench('Outdoor north bench - four places',dx,dy+.98,F,2.95)
|
||||
south_start=set(col.objects);bench('Outdoor south bench - four places',dx,dy-.98,F,2.95)
|
||||
for o in set(col.objects)-south_start:
|
||||
if 'backrest' in o.name:o.location.y=dy-1.19
|
||||
for x in [dx-1.94,dx+1.94]:
|
||||
box('Outdoor end chair seat',(x,dy,F+.45),(.47,.47,.06),wood,.012)
|
||||
for a in [-.17,.17]:
|
||||
for b in [-.17,.17]:box('Outdoor end chair leg',(x+a,dy+b,F+.21),(.038,.038,.42),woodz)
|
||||
box('Outdoor end chair back',(x+(-.21 if x<dx else .21),dy,F+.75),(.055,.47,.36),wood,.01)
|
||||
for x in [7.15,12.85]:
|
||||
for y in [.75,6.25]:box('Gathering pergola post',(x,y,F+1.38),(.12,.12,2.76),woodz,.006)
|
||||
for y in [.75,6.25]:box('Gathering pergola beam',(10,y,F+2.76),(6,.12,.20),wood,.005)
|
||||
for x in [7.1+i*.35 for i in range(18)]:box('Gathering pergola shade slat',(x,3.5,F+2.88),(.065,5.8,.14),wood)
|
||||
# Nearby movable tea counter and shared bowls.
|
||||
box('TEA AND MATCHA - timber preparation cart',(11.6,5.74,F+.88),(1.55,.58,.06),wood,.016)
|
||||
for x in [11,12.2]:
|
||||
for y in [5.55,5.93]:box('Tea cart frame',(x,y,F+.43),(.045,.045,.86),woodz)
|
||||
box('Tea cart storage shelf',(11.6,5.74,F+.25),(1.46,.50,.04),wood)
|
||||
rod('Tea kettle',(11.15,5.72,F+.92),(11.15,5.72,F+1.13),.10,metal,sides=24)
|
||||
for x in [11.5,11.76,12.02]:rod('Matcha ceramic bowl',(x,5.73,F+.92),(x,5.73,F+1.01),.075,cream,r2=.095,sides=24)
|
||||
for x in [8.9,9.7,10.5,11.3]:
|
||||
for y in [dy-.28,dy+.28]:rod('Shared table cup',(x,y,F+.81),(x,y,F+.89),.037,cream,sides=16)
|
||||
# Table tennis has its own level deck, clear of vehicles; folding halves remain editable.
|
||||
px,py,pz=11,-8.5,.80;deck('PLAY DECK',px,py,5.5,6.5,pz)
|
||||
for y in [py-.695,py+.695]:
|
||||
box('FOLDING TABLE TENNIS - hinged half',(px,y,pz+.76),(1.525,1.37,.045),sage,.004)
|
||||
for x in [px-.50,px+.50]:
|
||||
box('Table tennis folding leg',(x,y,pz+.38),(.035,.035,.71),steel,.004)
|
||||
rod('Table tennis caster',(x-.025,y,pz+.035),(x+.025,y,pz+.035),.034,black,sides=12)
|
||||
for x in [px-.746,px+.746,px]:box('Table tennis white longitudinal line',(x,py,pz+.784),(.012,2.73,.002),white)
|
||||
for y in [py-1.36,py+1.36]:box('Table tennis white end line',(px,y,pz+.785),(1.52,.018,.002),white)
|
||||
for x in [px-.82,px+.82]:rod('Table tennis net post',(x,py,pz+.76),(x,py,pz+.915),.007,steel,sides=8)
|
||||
for i in range(7):rod('Table tennis horizontal net cord',(px-.82,py,pz+.77+i*.024),(px+.82,py,pz+.77+i*.024),.0015,white,sides=5)
|
||||
for i in range(43):rod('Table tennis vertical net cord',(px-.8+i*.038,py,pz+.77),(px-.8+i*.038,py,pz+.914),.0012,white,sides=5)
|
||||
for i,x in enumerate([8.05,7.70,7.35]):
|
||||
top=.62-.18*i;box('Play deck access step',(x,-7,top-.08),(.36,1.2,.16),wood,.009)
|
||||
path('Play deck pedestrian connection',[(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],1.1)
|
||||
# A reserved footprint only: no court surface or working hoop is built now.
|
||||
future=bpy.data.materials['Future shadow - faint sage outline']
|
||||
for a,b in [((8,-41),(18,-41)),((18,-41),(18,-31)),((18,-31),(8,-31)),((8,-31),(8,-41))]:
|
||||
a,b=Vector(a),Vector(b)
|
||||
for i in range(10):
|
||||
p=a+(b-a)*i/10;q=a+(b-a)*(i+.55)/10
|
||||
beam('FUTURE BASKETBALL - dashed reserve',(p.x,p.y,ground(p.x,p.y)+.09),(q.x,q.y,ground(q.x,q.y)+.09),.07,.04,future)
|
||||
# Walking loop: joins the quiet room and garden without crossing building reserves.
|
||||
loop=[(-11,14.7),(-14,18),(-11,23),(1,24),(13,23),(20,19),(19,10),(30,10),(33,3),(33,-10),(27,-13),(20,-11),(17,-4),(14,1),(14,10),(19,10)]
|
||||
path('SHADED WALKING LOOP - garden and quiet room',loop,1.05)
|
||||
path('Walking loop return to home',[(14,1),(13,4),(13,7),(6,7)],1.05)
|
||||
path('Walking loop home to meditation',[(-1,24),(-7,21),(-14,18)],1.05)
|
||||
bench('QUIET GARDEN BENCH',30,10.9,ground(30,10.9)+.07,1.8)
|
||||
for x in [29,31]:box('Quiet bench shade post',(x,11,ground(x,11)+1.2),(.10,.10,2.4),woodz,.006)
|
||||
for x in [28.9+i*.26 for i in range(9)]:box('Quiet bench timber shade slat',(x,11,ground(30,11)+2.45),(.06,1.6,.10),wood)
|
||||
# Everyday food: clear prep counter, pantry and compact freezer along the south wall.
|
||||
box('BREAKFAST - clear preparation counter',(.22,-2.52,F+.94),(1.15,.73,.055),stone,.013)
|
||||
box('Breakfast counter timber base',(.22,-2.55,F+.45),(1.10,.62,.89),wood,.009)
|
||||
box('Breakfast chopping board',(.30,-2.48,F+.982),(.42,.30,.025),wood,.008)
|
||||
for x in [-.19,.02]:rod('Breakfast cereal jar',(x,-2.73,F+.98),(x,-2.73,F+1.18),.066,cream,sides=20)
|
||||
box('PANTRY - tall timber cabinet',(1.32,-2.55,F+1.06),(.85,.65,2.12),wood,.014)
|
||||
for x in [1.12,1.52]:box('Pantry door pull',(x,-2.21,F+1.05),(.023,.028,.23),steel,.004)
|
||||
box('FREEZER - compact upright',(2.27,-2.55,F+.66),(.72,.69,1.32),cream,.025)
|
||||
box('Freezer insulated door',(2.27,-2.19,F+.68),(.68,.05,1.23),metal,.014)
|
||||
box('Freezer door handle',(2.52,-2.14,F+.89),(.025,.04,.32),steel,.007)
|
||||
for x in [2.04,2.5]:box('Freezer ventilation clearance foot',(x,-2.54,F+.035),(.065,.48,.07),black)
|
||||
hx,hy=29.8,1.5;hz=ground(hx,hy)+.08
|
||||
box('HARVEST BENCH - garden sorting surface',(hx,hy,hz+.90),(.78,2.05,.065),wood,.018)
|
||||
for x in [hx-.28,hx+.28]:
|
||||
for y in [hy-.81,hy+.81]:box('Harvest bench leg',(x,y,hz+.43),(.06,.06,.86),woodz,.006)
|
||||
box('Harvest bench lower storage',(hx,hy,hz+.22),(.70,1.95,.045),wood)
|
||||
for y in [hy-.6,hy+.10]:
|
||||
box('Harvest timber produce crate',(hx,y,hz+1.045),(.57,.48,.24),wood,.012)
|
||||
box('Harvest crate dark interior',(hx,y,hz+1.171),(.49,.4,.01),bpy.data.materials['Damp brown garden soil'])
|
||||
layout={'training':[-12.2,-18,4.8,5.4],'outdoor_table':[10,3.5,6,5.8],'table_tennis':[11,-8.5,5.5,6.5],'future_basketball':[13,-36,10,10],'walking_loop':loop,'harvest_bench':[29.8,1.5], 'outdoor_seats':10}
|
||||
json.dump(layout,open(OUT+'/daily-life-layout.json','w'),indent=2)
|
||||
exec(compile(open(OUT+'/enhance_kitchen.py').read(),OUT+'/enhance_kitchen.py','exec'))
|
||||
# A single merged viewport copy for new geometry.
|
||||
verts=[];faces=[];mi=[];mats=[];mapping={}
|
||||
for o in col.objects:
|
||||
if o.type!='MESH':continue
|
||||
remap=[]
|
||||
for m in o.data.materials:
|
||||
if m not in mapping:mapping[m]=len(mats);mats.append(m)
|
||||
remap.append(mapping[m])
|
||||
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy());off=len(verts)
|
||||
verts.extend(tuple(world@v.co) for v in o.data.vertices)
|
||||
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));mi.append(remap[p.material_index])
|
||||
me=bpy.data.meshes.new('Daily life lightweight display');me.from_pydata(verts,[],faces);me.update()
|
||||
for m in mats:me.materials.append(m)
|
||||
me.polygons.foreach_set('material_index',mi)
|
||||
proxy=bpy.data.objects.new('FAST | Training gathering play and garden loop',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(proxy)
|
||||
cam=bpy.data.objects['PLAY TOUR camera']
|
||||
shots=[('25 / TRAINING & STRENGTH',(-8,-12,4),(-8.8,-12.8,3.8),(-12.2,-18,2),25),('26 / OUTDOOR TABLE & MATCHA',(15,9,4.8),(14,8,4.4),(10,3.5,1.6),28),('27 / PLAY & FUTURE BASKETBALL',(17,-1,5),(16,-2,4.7),(11,-8.5,1),29),('28 / WALKING & FOOD GARDEN',(35,16,8),(33,14,7),(27,3,1.3),30),('29 / EASY EVERYDAY FOOD',(-.3,.4,2.9),(.1,.1,2.8),(1,-2.5,2),24)]
|
||||
caption_col=bpy.data.collections['22 PLAY - guided camera tour']
|
||||
ref=max((o for o in caption_col.objects if o.type=='FONT'),key=lambda o:o.name)
|
||||
ref.scale=(0,0,0);ref.keyframe_insert('scale',frame=2881)
|
||||
shots.append(('30 / COPPER & MARBLE KITCHEN',(-.8,2.5,3.1),(-1.0,2.2,2.95),(-3.45,-1.2,1.9),25))
|
||||
chapters=json.load(open(OUT+'/tour-chapters-reading.json'))
|
||||
for idx,(title,a,b,target,lens) in enumerate(shots):
|
||||
start=2881+idx*120;end=start+119
|
||||
for f,p in [(start,a),(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)
|
||||
caption=ref.copy();caption.data=ref.data.copy();caption.animation_data_clear();caption.data.body=title;caption.name='Tour chapter '+title;caption_col.objects.link(caption)
|
||||
half=36/lens;caption.location=(-half*.91,half*9/16*.84,-2)
|
||||
for f,sz in [(1,0),(start-1,0),(start,1),(end,1),(end+1,0)]:
|
||||
caption.scale=(sz,sz,sz);caption.keyframe_insert('scale',frame=f)
|
||||
s.timeline_markers.new(title,frame=start);chapters.append({'title':title,'start':start,'end':end,'seconds':5})
|
||||
for title in caption_col.objects:
|
||||
if title.type=='FONT' and title.animation_data:
|
||||
for layer in title.animation_data.action.layers:
|
||||
for strip in layer.strips:
|
||||
for bag in strip.channelbags:
|
||||
for fc in bag.fcurves:
|
||||
if fc.data_path=='scale':
|
||||
for k in fc.keyframe_points:k.interpolation='CONSTANT'
|
||||
s.frame_end=3600;s.frame_set(3481);s.camera=cam
|
||||
json.dump(chapters,open(OUT+'/tour-chapters-daily-life.json','w'),indent=2)
|
||||
s['Daily life additions']='Covered exercise area, ten-place outdoor table and tea counter, folding table tennis, unbuilt basketball reserve, garden walking loop and quiet bench, breakfast counter, pantry/freezer and harvest bench.'
|
||||
s['Training design status']='Concept geometry only: dedicated exercise frame requires appropriate manufacturer rating/engineering and equipment clearances; future basketball rectangle is a reserved area, not a built level court.'
|
||||
report={'training_area':1,'pullup_bar':1,'wooden_rings':2,'adjustable_dumbbells':2,'adjustable_bench':1,'exercise_mats':2,'outdoor_seats':10,'tea_counter':1,'folding_table_tennis':1,'basketball_status':'Future reserve only','walking_loop':True,'quiet_bench':1,'breakfast_counter':1,'pantry':1,'freezer':1,'harvest_bench':1,'tour_chapters':30,'tour_seconds':150,'kitchen':kitchen_checks}
|
||||
json.dump(report,open(OUT+'/daily-life-checks.json','w'),indent=2)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-daily-life-tour.blend')
|
||||
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
|
||||
if o.type=='FONT':o.hide_render=True
|
||||
s.render.resolution_percentage=60;s.cycles.samples=24;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 fname,frame in [('training-area.png',2920),('outdoor-gathering.png',3035),('kitchen-copper-marble.png',3520)]:
|
||||
s.frame_set(frame);s.render.filepath=OUT+'/'+fname;bpy.ops.render.render(write_still=True)
|
||||
s.frame_set(3300);cam.animation_data_clear();cam.data.animation_data_clear();cam.location=(48,55,48);cam.rotation_euler=(Vector((2,-8,0))-cam.location).to_track_quat('-Z','Y').to_euler();cam.data.lens=37;s.render.filepath=OUT+'/daily-life-overview.png';bpy.ops.render.render(write_still=True)
|
||||
print('DAILY_LIFE_READY '+json.dumps(report),flush=True)
|
||||
102
source/samford-retreat-garage/add_reading_corner.py
Normal file
102
source/samford-retreat-garage/add_reading_corner.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import bpy,math,os,random,json,bmesh
|
||||
from mathutils import Vector
|
||||
from math import sin,cos,pi
|
||||
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene;random.seed(901)
|
||||
col=bpy.data.collections.new('31 Reading library - books and lounge chair');bpy.data.collections['DETAIL - full geometry for renders'].children.link(col)
|
||||
COL={'reading':col};active='reading'
|
||||
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']
|
||||
fabric=textured('Reading chair moss linen',(.16,.21,.12),(.34,.39,.25),(95,95,95),.92,.003)
|
||||
paper=mat('Books warm uncoated paper',(.74,.69,.54),.95)
|
||||
palette=[(.24,.31,.24),(.16,.25,.29),(.46,.22,.13),(.56,.41,.21),(.34,.26,.20),(.69,.61,.44),(.25,.17,.18),(.11,.16,.16),(.42,.45,.33),(.57,.35,.24)]
|
||||
covers=[mat('Cloth book cover %02d'%i,c,.88) for i,c in enumerate(palette)]
|
||||
ink=mat('Book spine gold print',(.56,.42,.23),.68)
|
||||
F=1.12;sx,sy=5.67,-.05
|
||||
# Two bays in one substantial wall bookcase, opening west into the main room.
|
||||
box('LIBRARY - solid timber bookshelf back',(sx+.18,sy,F+1.10),(.028,2.65,2.20),wood,.005)
|
||||
for y in [sy-1.32,sy,sy+1.32]:box('Library upright',(sx,y,F+1.10),(.39,.042,2.20),woodz,.009)
|
||||
levels=[.12,.51,.90,1.29,1.68,2.10]
|
||||
for z in levels:box('Library full length timber shelf',(sx,sy,F+z),(.41,2.69,.045),wood,.01)
|
||||
box('Library recessed plinth',(sx+.03,sy,F+.045),(.30,2.52,.09),woodz,.01)
|
||||
book_count=0
|
||||
for bay in range(2):
|
||||
start=sy-1.28+bay*1.32;end=start+1.23
|
||||
for row in range(5):
|
||||
y=start+.035
|
||||
while y<end-.07:
|
||||
w=random.uniform(.033,.078)
|
||||
if y+w>end:break
|
||||
h=random.uniform(.24,.325);d=random.uniform(.205,.285);z=F+levels[row]+.023;material=random.choice(covers);x=sx-.18+d/2+.025
|
||||
book_count+=1;n='Library book %03d'%book_count
|
||||
box(n+' paper block',(x,y+w/2,z+h/2),(d-.008,w-.008,h-.014),paper,.001)
|
||||
for yy in [y+.002,y+w-.002]:box(n+' cloth cover',(x,yy,z+h/2),(d+.012,.004,h),material,.0015)
|
||||
box(n+' visible spine',(x-d/2-.004,y+w/2,z+h/2),(.010,w,h),material,.002)
|
||||
for hz in [.055,h-.04]:box(n+' spine band',(x-d/2-.0095,y+w/2,z+hz),(.001,w*.7,.004),ink)
|
||||
y+=w+random.uniform(.008,.020)
|
||||
# A low, generously proportioned timber-and-linen reading chair faces the valley.
|
||||
before=set(col.objects);chairx,chairy=4.77,1.78
|
||||
box('READING CHAIR - timber seat frame',(0,0,F+.34),(.78,.79,.08),wood,.035)
|
||||
box('Reading chair deep linen seat',(0,.025,F+.46),(.70,.72,.18),fabric,.09)
|
||||
back=box('Reading chair reclined linen back',(0,-.33,F+.84),(.70,.17,.65),fabric,.085);back.rotation_euler.x=math.radians(-12)
|
||||
for x in [-.38,.38]:
|
||||
box('Reading chair timber arm',(x,-.01,F+.68),(.067,.84,.065),wood,.025)
|
||||
for y in [-.29,.29]:beam('Reading chair square timber leg',(x,y,F+.04),(x,y-.055,F+.65),.045,.045,woodz)
|
||||
beam('Reading chair back support',(x,-.29,F+.34),(x,-.46,F+1.12),.045,.045,woodz)
|
||||
angle=math.radians(48)
|
||||
from mathutils import Matrix
|
||||
rot=Matrix.Rotation(angle,4,'Z');trans=Matrix.Translation((chairx,chairy,0))
|
||||
for o in set(col.objects)-before:
|
||||
local=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy())
|
||||
o.matrix_world=trans@rot@local
|
||||
box('Reading nook side table top',(3.78,2.03,F+.54),(.48,.44,.055),wood,.02)
|
||||
for x in [3.60,3.96]:
|
||||
for y in [1.87,2.19]:box('Reading side table leg',(x,y,F+.26),(.035,.035,.52),woodz,.005)
|
||||
box('Reading table closed book',(3.78,2.02,F+.586),(.26,.18,.035),covers[2],.004)
|
||||
rod('Reading floor lamp base',(5.40,2.37,F),(5.40,2.37,F+.035),.15,steel,sides=32)
|
||||
rod('Reading floor lamp stem',(5.40,2.37,F+.035),(5.40,2.37,F+1.43),.012,steel,sides=16)
|
||||
lampmat=mat('Reading lamp undyed linen shade',(.68,.59,.42),.9)
|
||||
rod('Reading floor lamp linen shade',(5.40,2.37,F+1.32),(5.40,2.37,F+1.61),.22,lampmat,r2=.135,sides=48)
|
||||
ld=bpy.data.lights.new('Reading nook warm lamp','POINT');ld.energy=55;ld.color=(1,.69,.38);ld.shadow_soft_size=.18
|
||||
lo=bpy.data.objects.new(ld.name,ld);col.objects.link(lo);lo.location=(5.4,2.37,F+1.34)
|
||||
# One lightweight static display mesh keeps the existing fast tour responsive.
|
||||
verts=[];faces=[];mis=[];mats=[];index={}
|
||||
for o in col.objects:
|
||||
if o.type!='MESH':continue
|
||||
remap=[]
|
||||
for m in o.data.materials:
|
||||
if m not in index:index[m]=len(mats);mats.append(m)
|
||||
remap.append(index[m])
|
||||
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy())
|
||||
off=len(verts);verts.extend(tuple(world@v.co) for v in o.data.vertices)
|
||||
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));mis.append(remap[p.material_index])
|
||||
me=bpy.data.meshes.new('Reading corner lightweight display');me.from_pydata(verts,[],faces);me.update()
|
||||
for m in mats:me.materials.append(m)
|
||||
me.polygons.foreach_set('material_index',mis)
|
||||
o=bpy.data.objects.new('FAST | Reading library and chair',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(o)
|
||||
# Append a five-second final chapter showing the new reading corner.
|
||||
cam=bpy.data.objects['PLAY TOUR camera']
|
||||
for frame,loc,target in [(2761,(1.2,4.65,3.5),(4.7,.5,1.9)),(2880,(1.65,4.3,3.35),(4.7,.5,1.9))]:
|
||||
s.frame_set(frame);cam.location=loc;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);cam.data.lens=29;cam.data.keyframe_insert('lens',frame=frame)
|
||||
titles=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
|
||||
original=titles[-1];title=original.copy();title.data=original.data.copy();title.animation_data_clear();title.name='Tour chapter 24 - reading library';title.data.body='24 / BOOKS & A QUIET READING CORNER';bpy.data.collections['22 PLAY - guided camera tour'].objects.link(title)
|
||||
for f,scale in [(1,0),(2760,0),(2761,1),(2880,1)]:title.scale=(scale,)*3;title.keyframe_insert('scale',frame=f)
|
||||
if title.animation_data:
|
||||
action=title.animation_data.action
|
||||
for layer in action.layers:
|
||||
for strip in layer.strips:
|
||||
for bag in strip.channelbags:
|
||||
for fc in bag.fcurves:
|
||||
for k in fc.keyframe_points:k.interpolation='CONSTANT'
|
||||
s.timeline_markers.new('24 / BOOKS & READING',frame=2761);s.frame_end=2880;s.frame_set(2761);s.camera=cam
|
||||
s['Reading library']='Two-bay timber bookshelf filled with %d books; linen-and-timber reading chair, side table and reading lamp in the main living room.'%book_count
|
||||
report={'books':book_count,'bookshelf_bays':2,'reading_chairs':1,'tour_chapters':24,'tour_seconds':120,'fast_proxy_objects_added':1}
|
||||
json.dump(report,open(OUT+'/reading-corner-checks.json','w'),indent=2)
|
||||
chapters=json.load(open(OUT+'/tour-chapters.json'));chapters[20]['title']='21 / TESLA, SOLAR CHARGING & WORKSHOP';chapters.append({'title':'24 / BOOKS & A QUIET READING CORNER','start':2761,'end':2880,'seconds':5});json.dump(chapters,open(OUT+'/tour-chapters-reading.json','w'),indent=2)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-reading-tour.blend')
|
||||
for o in titles+[title]:o.hide_render=True
|
||||
s.render.resolution_percentage=60;s.cycles.samples=24;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';s.render.filepath=OUT+'/reading-corner.png';bpy.ops.render.render(write_still=True)
|
||||
print('READING_CORNER_DONE '+json.dumps(report),flush=True)
|
||||
11
source/samford-retreat-garage/bedroom-sleep-checks.json
Normal file
11
source/samford-retreat-garage/bedroom-sleep-checks.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"eight_sleep_cover": 1,
|
||||
"eight_sleep_hub": 1,
|
||||
"blackout_curtain_panels": 2,
|
||||
"curtain_closed_shape_keys": 2,
|
||||
"indoor_ac": 1,
|
||||
"outdoor_condenser": 1,
|
||||
"tour_chapters": 24,
|
||||
"tour_seconds": 120,
|
||||
"new_fast_display_meshes": 1
|
||||
}
|
||||
105
source/samford-retreat-garage/bedroom_sleep_setup.py
Normal file
105
source/samford-retreat-garage/bedroom_sleep_setup.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import bpy, math, os, json
|
||||
from mathutils import Vector, Matrix
|
||||
from math import sin, cos, pi
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
|
||||
detail=bpy.data.collections['DETAIL - full geometry for renders']
|
||||
col=bpy.data.collections.new('32 Bedroom - Eight Sleep blackout and air conditioning');detail.children.link(col)
|
||||
COL={'sleep':col};active='sleep'
|
||||
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'];steel=bpy.data.materials['Reusable dark steel bolts and shoes']
|
||||
linen=bpy.data.materials['Undyed flax linen'];stone=bpy.data.materials['Split sandstone facing - mineral grain']
|
||||
black=textured('Eight Sleep charcoal cover textile',(.027,.029,.033),(.065,.067,.071),(110,110,110),.91,.001)
|
||||
hubmat=mat('Eight Sleep hub graphite casing',(.042,.045,.049),.48)
|
||||
white=mat('Quiet AC warm white casing',(.79,.78,.73),.38)
|
||||
lining=mat('Opaque blackout curtain lining',(.095,.09,.08),1)
|
||||
curtainmat=textured('Blackout curtain oatmeal woven face',(.27,.24,.18),(.48,.43,.34),(100,100,20),.98,.002)
|
||||
F=1.12;bx=-12.5;by=-5
|
||||
cover=box('EIGHT SLEEP - fitted temperature regulating cover',(bx,by,1.727),(1.91,2.16,.065),black,.035)
|
||||
cover['Product']='Eight Sleep Pod cover and bedside Hub; approximate concept geometry, not a manufacturer CAD model.'
|
||||
box('Eight Sleep fitted cover north skirt',(bx,-3.914,1.65),(1.89,.022,.15),black,.018)
|
||||
for x in [-13.454,-11.546]:box('Eight Sleep fitted cover side skirt',(x,by,1.65),(.025,2.10,.15),black,.014)
|
||||
box('Bed folded undyed linen throw',(bx,-4.24,1.79),(1.83,.59,.07),linen,.025)
|
||||
hx,hy=-13.97,-5.60
|
||||
box('EIGHT SLEEP - bedside cooling Hub',(hx,hy,F+.245),(.29,.41,.47),hubmat,.045)
|
||||
box('Eight Sleep hub top lid',(hx,hy,F+.485),(.255,.365,.014),black,.028)
|
||||
box('Eight Sleep hub front intake',(hx,hy+.207,F+.24),(.22,.008,.32),black,.006)
|
||||
for z in range(18):box('Hub ventilation slot',(hx,hy+.213,F+.09+z*.016),(.205,.004,.004),steel,.001)
|
||||
for x in [hx-.10,hx+.10]:
|
||||
for y in [hy-.14,hy+.14]:box('Hub rubber isolating foot',(x,y,F+.014),(.033,.04,.028),black,.004)
|
||||
# Twin hoses stay near the bed head and clear of the circulation route.
|
||||
for dx in [-.02,.02]:
|
||||
points=[(hx+dx,hy-.19,F+.19),(hx+dx,-6.21,F+.14),(-13.40+dx,-6.21,F+.16),(-13.40+dx,-6.04,1.68)]
|
||||
for a,b in zip(points,points[1:]):rod('Eight Sleep water hose',a,b,.012,black,sides=12)
|
||||
labeldata=bpy.data.curves.new('Eight Sleep wordmark','FONT');labeldata.body='EIGHT SLEEP';labeldata.size=.032;labeldata.align_x='CENTER';labeldata.materials.append(white)
|
||||
label=bpy.data.objects.new('Eight Sleep Hub label',labeldata);col.objects.link(label);label.location=(hx,hy+.218,F+.405);label.rotation_euler=(pi/2,0,pi)
|
||||
# Pleated curtains: two real opaque layers; shape keys expose the closed position.
|
||||
box('Blackout curtain ceiling track',(-12.65,-2.69,4.22),(5.45,.06,.055),steel,.009)
|
||||
box('Blackout curtain timber pelmet',(-12.65,-2.73,4.245),(5.5,.23,.20),wood,.01)
|
||||
curtains=[]
|
||||
for side,edge,sign in [('left',-15.35,1),('right',-9.95,-1)]:
|
||||
vs=[];fs=[];steps=64
|
||||
for z in [1.14,4.18]:
|
||||
for i in range(steps+1):
|
||||
u=i/steps;vs.append((edge+sign*.82*u,-2.68+.065*cos(u*2*pi*8),z))
|
||||
for i in range(steps):fs.append((i,i+1,steps+i+2,steps+i+1))
|
||||
o=mesh('BLACKOUT CURTAIN - '+side,vs,fs,curtainmat);o.data.materials.append(lining)
|
||||
o.shape_key_add(name='Basis - open for viewing');closed=o.shape_key_add(name='Closed - blackout')
|
||||
for i,v in enumerate(closed.data):v.co.x=edge+sign*2.73*(i%(steps+1))/steps
|
||||
closed.value=0.0;o.active_shape_key_index=0
|
||||
m=o.modifiers.new('Opaque blackout lining','SOLIDIFY');m.thickness=.007;m.material_offset=1
|
||||
o['How to close']='Object Data Properties > Shape Keys > Closed - blackout: set Value to 1 on both panels. DETAIL collection must be enabled for editing.'
|
||||
curtains.append(o)
|
||||
# Indoor split head on the west wall near the foot of the bed.
|
||||
ax,ay,az=-15.57,-3.48,3.67
|
||||
box('AIR CONDITIONER - bedroom indoor split head',(ax,ay,az),(.23,.92,.30),white,.055)
|
||||
box('Air conditioner dark outlet',(ax+.118,ay,az-.09),(.014,.79,.065),black,.008)
|
||||
for z in [az-.11,az-.075]:box('Air conditioner adjustable louvre',(ax+.13,ay,z),(.07,.78,.012),white,.003)
|
||||
box('Air conditioner status panel',(ax+.12,ay-.29,az+.045),(.009,.10,.035),hubmat,.003)
|
||||
box('AC remote on bedroom wall',(-15.67,-4.18,2.33),(.038,.065,.15),white,.008)
|
||||
box('AC remote display',(-15.647,-4.18,2.35),(.009,.048,.045),hubmat,.002)
|
||||
# External condenser on isolation feet, behind the sleeping pavilion.
|
||||
cx,cy,cz=-14.70,-8.12,.79
|
||||
box('AC outdoor unit stable pad',(cx,cy,.39),(1.03,.67,.10),stone,.015)
|
||||
box('AIR CONDITIONER - outdoor condenser',(cx,cy,cz),(.88,.39,.64),white,.035)
|
||||
for x in [cx-.30,cx+.30]:box('AC outdoor vibration mount',(x,cy,.46),(.085,.32,.04),black,.005)
|
||||
rod('AC outdoor circular fan grille',(cx-.12,cy-.202,cz),(cx-.12,cy-.215,cz),.24,black,sides=48)
|
||||
for i in range(11):
|
||||
x=cx-.34+i*.044
|
||||
half=math.sqrt(max(0,.235**2-(x-(cx-.12))**2))
|
||||
rod('Condenser grille bar',(x,cy-.22,cz-half),(x,cy-.22,cz+half),.0045,steel,sides=6)
|
||||
for z in range(9):box('Condenser service ventilation',(cx+.30,cy-.20,cz-.20+z*.048),(.14,.008,.011),steel,.002)
|
||||
pipepoints=[(-15.94,-3.48,3.65),(-15.94,-7.72,3.65),(-15.94,-7.72,.8),(-15.05,-7.72,.8),(-15.05,-7.92,.8)]
|
||||
for a,b in zip(pipepoints,pipepoints[1:]):beam('AC accessible insulated service trunking',a,b,.075,.065,white)
|
||||
rod('AC condensate drain down exterior',(-15.94,-3.5,3.52),(-15.94,-3.5,.5),.013,white,sides=10)
|
||||
s['Bedroom sleep setup']='Eight Sleep Pod-style fitted cover with ventilated bedside Hub, lined blackout curtains with closed shape keys, indoor split AC and outdoor condenser. Concept placement; final product clearances and HVAC sizing are unresolved.'
|
||||
# Preserve fast tour performance with one display proxy for the new static pieces.
|
||||
verts=[];faces=[];indices=[];materials=[];mapping={}
|
||||
for o in col.objects:
|
||||
if o.type!='MESH':continue
|
||||
remap=[]
|
||||
for m in o.data.materials:
|
||||
if m not in mapping:mapping[m]=len(materials);materials.append(m)
|
||||
remap.append(mapping[m])
|
||||
world=Matrix.LocRotScale(o.location.copy(),o.rotation_euler.to_quaternion(),o.scale.copy());off=len(verts)
|
||||
verts.extend(tuple(world@v.co) for v in o.data.vertices)
|
||||
for p in o.data.polygons:faces.append(tuple(off+i for i in p.vertices));indices.append(remap[p.material_index])
|
||||
me=bpy.data.meshes.new('Bedroom sleep equipment lightweight display');me.from_pydata(verts,[],faces);me.update()
|
||||
for m in materials:me.materials.append(m)
|
||||
me.polygons.foreach_set('material_index',indices)
|
||||
proxy=bpy.data.objects.new('FAST | Bedroom sleep equipment',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(proxy)
|
||||
# Refine the existing bedroom chapter without changing the two-minute tour length.
|
||||
cam=bpy.data.objects['PLAY TOUR camera']
|
||||
for frame,loc,target in [(841,(-10.70,-6.95,3.30),(-13.10,-4.60,1.97)),(960,(-10.95,-6.73,3.20),(-13.10,-4.52,1.97))]:
|
||||
s.frame_set(frame);cam.location=loc;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);cam.data.lens=20;cam.data.keyframe_insert('lens',frame=frame)
|
||||
s.frame_set(841);s.camera=cam
|
||||
report={'eight_sleep_cover':1,'eight_sleep_hub':1,'blackout_curtain_panels':2,'curtain_closed_shape_keys':2,'indoor_ac':1,'outdoor_condenser':1,'tour_chapters':24,'tour_seconds':120,'new_fast_display_meshes':1}
|
||||
json.dump(report,open(OUT+'/bedroom-sleep-checks.json','w'),indent=2)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-bedroom-tour.blend')
|
||||
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
|
||||
if o.type=='FONT':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';s.render.filepath=OUT+'/bedroom-sleep-setup.png';bpy.ops.render.render(write_still=True)
|
||||
print('BEDROOM_SLEEP_READY '+json.dumps(report),flush=True)
|
||||
54
source/samford-retreat-garage/create_garden_plan.py
Normal file
54
source/samford-retreat-garage/create_garden_plan.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from pathlib import Path
|
||||
import json,html
|
||||
O=Path(__file__).parent;d=json.loads((O/'garden-flow-layout.json').read_text());p=[]
|
||||
p.append('<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1180" viewBox="0 0 1400 1180"><rect width="1400" height="1180" fill="#f3f0e7"/><style>text{font-family:Arial,sans-serif;fill:#30433b}.sub{fill:#68756b}</style><text x="60" y="58" font-size="31" font-weight="600">A home held together by its garden.</text><text x="60" y="91" font-size="16" class="sub">SAMFORD VALLEY / Aligned pavilions, soft garden curves and room to grow · Concept 004</text>')
|
||||
def pt(x,y):return (600+x*13,500-y*13)
|
||||
def line(pts,color,w,dash=''):
|
||||
q=' '.join(f'{pt(x,y)[0]:.1f},{pt(x,y)[1]:.1f}' for x,y in pts);p.append(f'<polyline points="{q}" fill="none" stroke="{color}" stroke-width="{w}" stroke-linejoin="round" stroke-linecap="round" {dash}/>')
|
||||
def rect(cx,cy,w,h,fill='#d9c5a5',dash=''):
|
||||
x,y=pt(cx-w/2,cy+h/2);p.append(f'<rect x="{x}" y="{y}" width="{w*13}" height="{h*13}" fill="{fill}" stroke="#657666" stroke-width="1.5" {dash}/>')
|
||||
def label(x,y,lines,size=11):
|
||||
X,Y=pt(x,y)
|
||||
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{html.escape(t)}</text>')
|
||||
def ellipse(cx,cy,rx,ry,fill):
|
||||
x,y=pt(cx,cy);p.append(f'<ellipse cx="{x}" cy="{y}" rx="{rx*13}" ry="{ry*13}" fill="{fill}"/>')
|
||||
line([(0,-23),(0,28)],'#c1cbbc',1,'stroke-dasharray="4 6"')
|
||||
ellipse(0,13.2,7.8,4.6,'#c7d4b5')
|
||||
for path in d['paths']:
|
||||
color='#c9bda6' if 'VEHICLE' in path['name'] else ('#b59b75' if path['kind']=='roofed walk' else '#d9d2bf')
|
||||
line(path['points'],color,path['width']*13)
|
||||
rect(2,-12.5,12,6,'#c9bda6')
|
||||
for bed in d['beds']:
|
||||
x,y=bed['center'];rx,ry=bed['radii'];ellipse(x,y,rx,ry,'#8fa981')
|
||||
for xx in [-.5,0,.5]:ellipse(x+xx*rx,y,.12,.12,'#d5b6b5')
|
||||
rect(*d['main']);rect(0,4.2,12,2.4,'#c5cfb6');label(0,4.4,['VERANDA'],11)
|
||||
rect(-3.8,.1,2.2,.95,'#f5f2e9');label(1,1,['MAIN HOUSE','Kitchen · dining','Living / reading'],10)
|
||||
rect(*d['parents']);label(-11.8,-4.4,['PARENTS','Bedroom + bath'],11)
|
||||
rect(-7.75,-1.8,3.5,1.2,'#c8bba5')
|
||||
rect(*d['meditation']);label(-11.8,12.5,['MEDITATION','ROOM'],10)
|
||||
for idx,shape in enumerate(d['future']):
|
||||
rect(*shape,'#e7ecdf','stroke-dasharray="5 5"');label(shape[0],shape[1]+.6,['FUTURE',f'PAVILION {idx+1}'],10)
|
||||
label(0,13.5,['SHARED','GARDEN COURT'],11)
|
||||
label(0,28,['ONE CENTRAL AXIS'],11)
|
||||
label(-16.7,18.6,['Curved covered','timber breezeway'],11)
|
||||
line([(-16.7,17),(-17,8.5),(-8.3,7.2)],'#8b9785',1)
|
||||
rect(*d['outdoor_table'],'#d9c49f');rect(10,3.5,3.2,1.03,'#a48663');label(10,9,['TABLE FOR TEN','Tea + matcha'],10)
|
||||
rect(*d['garage'],'#c7c2b3');rect(-1.7,-19.1,1.9,4.35,'#9da99c');label(1.5,-18.5,['GARAGE','Workshop','Bikes'],10)
|
||||
rect(*d['training'],'#c1ceb1');label(-9.4,-17.9,['TRAINING','Bar + rings','Weights + mats'],10)
|
||||
rect(*d['solar'],'#849a9d');label(9.4,-18.7,['GROUND SOLAR'],9)
|
||||
label(0,-25,['Aligned garage, training and energy zone'],11)
|
||||
x,y=d['sauna'];ellipse(x,y,2.2,2.0,'#ba9b75');label(x,y+.15,['SAUNA'],10)
|
||||
rect(-27,6.8,1.3,1.3,'#b0c5c1');label(-29,10,['Outdoor shower'],10)
|
||||
for x in [-23.95,-26.25]:ellipse(x,-8,1,1,'#6d8563')
|
||||
label(-25,-10.8,['Rainwater tanks'],10)
|
||||
rect(24.2,-7.5,4.3,5,'#c3d7c2');label(24.2,-7.4,['GREENHOUSE'],9)
|
||||
for x in [23.8,27.5]:
|
||||
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#ad9573')
|
||||
label(25.5,8.5,['FOOD GARDEN'],11)
|
||||
rect(29.8,1.5,.78,2.05,'#a78e6b');label(33.5,1.5,['Harvest bench'],10)
|
||||
rect(30,10.9,1.8,.45,'#a78e6b');label(31.5,16,['Quiet bench'],10)
|
||||
label(12,-7,['FLOWER','GARDEN'],10)
|
||||
rect(*d['future_basketball'],'#edf0e6','stroke-dasharray="6 6"');label(13,-35,['FUTURE','BASKETBALL','Unbuilt reserve'],11)
|
||||
label(27,-29,['4 m gravel access'],11)
|
||||
p.extend(['<path d="M1250 220V153m0 0l-9 18m9-18l9 18" fill="none" stroke="#30433b" stroke-width="3"/><text x="1250" y="140" text-anchor="middle" font-size="17">N</text>','<line x1="65" y1="1030" x2="195" y2="1030" stroke="#30433b" stroke-width="4"/><text x="65" y="1060" font-size="13">10 metres</text>','<line x1="60" y1="1090" x2="1340" y2="1090" stroke="#c5cebd"/><text x="60" y="1120" font-size="14">13 planted garden beds · Smooth pedestrian routes · Future pavilions remain shadows · Table-tennis deck removed</text>','<text x="60" y="1147" font-size="13" class="sub">Concept only: illustrative terrain, no surveyed boundary. Plant forms and structural connections require site-specific design development.</text></svg>'])
|
||||
(O/'garden-flow-plan.svg').write_text(''.join(p))
|
||||
64
source/samford-retreat-garage/create_site_plan.py
Normal file
64
source/samford-retreat-garage/create_site_plan.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from pathlib import Path
|
||||
from html import escape
|
||||
import json
|
||||
O=Path(__file__).parent
|
||||
p=['<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1120" viewBox="0 0 1400 1120"><rect width="1400" height="1120" fill="#f1eee5"/><style>text{font-family:Arial,sans-serif;fill:#263a34}</style><text x="60" y="60" font-size="32" font-weight="600">A home for movement, connection and quiet.</text><text x="60" y="95" font-size="17">SAMFORD VALLEY / Updated concept plan · 10 September 2026 · Version 003</text>']
|
||||
def pt(x,y):return (630+13*x,470-13*y)
|
||||
def label(x,y,lines,size=12):
|
||||
X,Y=pt(x,y)
|
||||
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{escape(t)}</text>')
|
||||
def rect(cx,cy,w,d,fill,stroke='#526154',dash=''):
|
||||
X,Y=pt(cx-w/2,cy+d/2);p.append(f'<rect x="{X}" y="{Y}" width="{w*13}" height="{d*13}" fill="{fill}" stroke="{stroke}" stroke-width="2" {dash}/>')
|
||||
def line(points,color,width):
|
||||
pts=' '.join(f'{pt(x,y)[0]},{pt(x,y)[1]}' for x,y in points);p.append(f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
|
||||
layout=json.loads((O/'daily-life-layout.json').read_text())
|
||||
line(layout['walking_loop'],'#adc49a',14)
|
||||
line([(14,1),(13,4),(13,7),(6,7)],'#adc49a',14)
|
||||
line([(-1,24),(-7,21),(-14,18)],'#adc49a',14)
|
||||
label(9,27.4,['SHADED WALKING LOOP'],12)
|
||||
line([(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)],'#c9bca1',52)
|
||||
rect(-1,-12.5,12,6,'#c9bca1',stroke='#c9bca1')
|
||||
line([(-3,-10),(-2,-6),(-5.5,-4),(-6.5,-2.8)],'#d5cdbb',12)
|
||||
line([(-3,5.4),(-3,9),(-11,9)],'#b5a88c',16)
|
||||
rect(0,0,12,6,'#ddc8a9');label(1,.7,['SHARED HOME','Kitchen · dining'],10)
|
||||
rect(0,4.2,12,2.4,'#c8d0b8');label(0,4.3,['VERANDA'],11)
|
||||
rect(-7.75,-1.8,3.5,1.2,'#c7bfae');label(-7.8,1.5,['Short link'],10)
|
||||
rect(-11.8,-4.9,8,5,'#ddc8a9');label(-11.8,-4.5,['PARENTS','Bedroom + bath'],11)
|
||||
rect(-11,12,4.5,4.8,'#c9c1a5');label(-11,12.4,['QUIET','ROOM'],11)
|
||||
X,Y=pt(-11,12);p.append(f'<circle cx="{X}" cy="{Y+24}" r="5" fill="#626257"/>')
|
||||
for i,(x,y) in enumerate([(-3,17),(7,15)]):
|
||||
rect(x,y,4.5,4.8,'#e6e8de',stroke='#98a394',dash='stroke-dasharray="5 5"');label(x,y+.6,['FUTURE',f'SPACE {i+1}'],11)
|
||||
label(2,21.4,['Faint outlines only / build when needed'],12)
|
||||
X,Y=pt(-23,2);p.append(f'<ellipse cx="{X}" cy="{Y}" rx="29" ry="25" fill="#bc9d75" stroke="#526154" stroke-width="2"/>');label(-23,2,['SAUNA'],10)
|
||||
rect(-27,4.6,1.3,1.3,'#b6c4c5');label(-27,7.5,['Outdoor shower'],11)
|
||||
for x in [-23.95,-26.25]:
|
||||
X,Y=pt(x,-8);p.append(f'<circle cx="{X}" cy="{Y}" r="13" fill="#6b7f61"/>')
|
||||
label(-25,-10.5,['Rainwater tanks'],11)
|
||||
rect(-3,-19,8,7,'#bcb9ac');label(-3,-18.6,['GARAGE','8 × 7 m'],12)
|
||||
rect(-4.7,-19.1,1.9,4.35,'#788c83');rect(-.47,-21.5,2.5,.76,'#ab9673')
|
||||
label(-3,-24,['One car · workbench · bike racks'],12)
|
||||
label(20,-25,['4 m gravel access road'],13)
|
||||
rect(24.2,-7.5,4.3,5,'#c3d6c1');label(24.2,-7.4,['GREENHOUSE'],9)
|
||||
for x in [23.8,27.5]:
|
||||
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#b29b73')
|
||||
label(25.5,7.5,['FOOD GARDEN'],12)
|
||||
rect(13.2,-17.7,7.3,5.2,'#71848b');label(13.2,-17.2,['GROUND SOLAR'],10)
|
||||
label(34,-42,['Site approach'],12)
|
||||
rect(*layout['training'],'#b9c8ad');label(-12.2,-16.8,['COVERED','TRAINING','4.8 × 5.4 m'],10)
|
||||
line([(-8.65,-17),(-8.4,-15),(-6.4,-13.4)],'#d5cdbb',12)
|
||||
label(-15.8,-23.2,['Bar · rings · weights','Bench · movable mats'],11)
|
||||
rect(*layout['outdoor_table'],'#d9c19b');rect(10,3.5,3.2,1.03,'#ab8962')
|
||||
rect(10,4.48,2.95,.43,'#bda078');rect(10,2.52,2.95,.43,'#bda078')
|
||||
rect(6.5,4.2,1,1.5,'#c8d0b8');label(10,9,['OUTDOOR TABLE','10 places + tea counter'],11)
|
||||
rect(*layout['table_tennis'],'#ced8c4');rect(11,-8.5,1.525,2.74,'#608570')
|
||||
label(11,-5.9,['TABLE TENNIS'],10);label(11,-12.8,['Folding table / level deck'],10)
|
||||
line([(7.1,-7),(7,-4),(6.4,1.3),(7.1,3.5)],'#d5cdbb',12)
|
||||
rect(*layout['future_basketball'],'#eceee4',stroke='#81947c',dash='stroke-dasharray="7 7"')
|
||||
label(13,-35,['FUTURE','BASKETBALL','10 × 10 m reserve'],11)
|
||||
label(13,-43,['Unbuilt / separate from driveway'],11)
|
||||
rect(30,10.9,1.8,.45,'#a58a66');label(32,14,['QUIET BENCH'],11)
|
||||
rect(29.8,1.5,.78,2.05,'#a58a66');label(35,2.4,['HARVEST','BENCH'],11)
|
||||
label(4,-4.3,['Breakfast · pantry · freezer'],10)
|
||||
rect(-3.8,.1,2.2,.95,'#f4f1e9')
|
||||
p.extend(['<path d="M1250 220V153m0 0l-9 18m9-18l9 18" fill="none" stroke="#263a34" stroke-width="3"/><text x="1250" y="140" text-anchor="middle" font-size="17">N</text>','<line x1="70" y1="970" x2="200" y2="970" stroke="#263a34" stroke-width="4"/><text x="70" y="999" font-size="13">10 metres</text>','<line x1="60" y1="1040" x2="1340" y2="1040" stroke="#bdc3b6"/><text x="60" y="1070" font-size="15">The Blender presentation fades one facade of each building so you can see inside. Walls remain in the model.</text>','<text x="60" y="1096" font-size="13">Concept layout only. The road approach is illustrative; no surveyed property boundary or approved access is implied.</text></svg>'])
|
||||
(O/'retreat-garage-plan.svg').write_text(''.join(p))
|
||||
34
source/samford-retreat-garage/daily-life-checks.json
Normal file
34
source/samford-retreat-garage/daily-life-checks.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"training_area": 1,
|
||||
"pullup_bar": 1,
|
||||
"wooden_rings": 2,
|
||||
"adjustable_dumbbells": 2,
|
||||
"adjustable_bench": 1,
|
||||
"exercise_mats": 2,
|
||||
"outdoor_seats": 10,
|
||||
"tea_counter": 1,
|
||||
"folding_table_tennis": 1,
|
||||
"basketball_status": "Future reserve only",
|
||||
"walking_loop": true,
|
||||
"quiet_bench": 1,
|
||||
"breakfast_counter": 1,
|
||||
"pantry": 1,
|
||||
"freezer": 1,
|
||||
"harvest_bench": 1,
|
||||
"tour_chapters": 30,
|
||||
"tour_seconds": 150,
|
||||
"kitchen": {
|
||||
"oven": 1,
|
||||
"induction_zones": 4,
|
||||
"copper_stockpot": 1,
|
||||
"copper_saucepan": 1,
|
||||
"copper_frying_pan": 1,
|
||||
"marble_island_m": [
|
||||
2.2,
|
||||
0.95
|
||||
],
|
||||
"island_rear_work_aisle_m": 1.67,
|
||||
"old_simplified_kitchen_removed": true,
|
||||
"fast_furniture_refreshed": true
|
||||
}
|
||||
}
|
||||
97
source/samford-retreat-garage/daily-life-layout.json
Normal file
97
source/samford-retreat-garage/daily-life-layout.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"training": [
|
||||
-12.2,
|
||||
-18,
|
||||
4.8,
|
||||
5.4
|
||||
],
|
||||
"outdoor_table": [
|
||||
10,
|
||||
3.5,
|
||||
6,
|
||||
5.8
|
||||
],
|
||||
"table_tennis": [
|
||||
11,
|
||||
-8.5,
|
||||
5.5,
|
||||
6.5
|
||||
],
|
||||
"future_basketball": [
|
||||
13,
|
||||
-36,
|
||||
10,
|
||||
10
|
||||
],
|
||||
"walking_loop": [
|
||||
[
|
||||
-11,
|
||||
14.7
|
||||
],
|
||||
[
|
||||
-14,
|
||||
18
|
||||
],
|
||||
[
|
||||
-11,
|
||||
23
|
||||
],
|
||||
[
|
||||
1,
|
||||
24
|
||||
],
|
||||
[
|
||||
13,
|
||||
23
|
||||
],
|
||||
[
|
||||
20,
|
||||
19
|
||||
],
|
||||
[
|
||||
19,
|
||||
10
|
||||
],
|
||||
[
|
||||
30,
|
||||
10
|
||||
],
|
||||
[
|
||||
33,
|
||||
3
|
||||
],
|
||||
[
|
||||
33,
|
||||
-10
|
||||
],
|
||||
[
|
||||
27,
|
||||
-13
|
||||
],
|
||||
[
|
||||
20,
|
||||
-11
|
||||
],
|
||||
[
|
||||
17,
|
||||
-4
|
||||
],
|
||||
[
|
||||
14,
|
||||
1
|
||||
],
|
||||
[
|
||||
14,
|
||||
10
|
||||
],
|
||||
[
|
||||
19,
|
||||
10
|
||||
]
|
||||
],
|
||||
"harvest_bench": [
|
||||
29.8,
|
||||
1.5
|
||||
],
|
||||
"outdoor_seats": 10
|
||||
}
|
||||
198
source/samford-retreat-garage/daily-life-verification.json
Normal file
198
source/samford-retreat-garage/daily-life-verification.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"chapters": 30,
|
||||
"end_frame": 3600,
|
||||
"fps": 24,
|
||||
"missing": [],
|
||||
"caption_issues": [],
|
||||
"caption_samples": [
|
||||
{
|
||||
"frame": 31,
|
||||
"titles": [
|
||||
"01 / THE SPACIOUS HOMESTEAD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 151,
|
||||
"titles": [
|
||||
"02 / LIVING PAVILION & VERANDA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 271,
|
||||
"titles": [
|
||||
"03 / KITCHEN & EXPOSED STRUCTURE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 391,
|
||||
"titles": [
|
||||
"04 / DINING & OPEN LIVING"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 511,
|
||||
"titles": [
|
||||
"05 / ONE SERVER - LOCAL AI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 631,
|
||||
"titles": [
|
||||
"06 / SHORT CORRIDOR TO PRIVATE ROOMS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 751,
|
||||
"titles": [
|
||||
"07 / ADJACENT BEDROOM WING"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 871,
|
||||
"titles": [
|
||||
"08 / ENCLOSED BEDROOM"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 991,
|
||||
"titles": [
|
||||
"09 / BATHROOM - SEPARATE LOBBY DOOR"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1111,
|
||||
"titles": [
|
||||
"10 / FINNISH SAUNA & OUTDOOR SHOWER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1231,
|
||||
"titles": [
|
||||
"11 / SAUNA BENCHES & STONE HEATER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1351,
|
||||
"titles": [
|
||||
"12 / OUTDOOR SHOWER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1471,
|
||||
"titles": [
|
||||
"13 / TWO RAINWATER TANKS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1591,
|
||||
"titles": [
|
||||
"14 / ROOFTOP SOLAR & STARLINK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1711,
|
||||
"titles": [
|
||||
"15 / SOLAR ARRAY & ENERGY STORAGE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1831,
|
||||
"titles": [
|
||||
"16 / FOOD GARDEN & GREENHOUSE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 1951,
|
||||
"titles": [
|
||||
"17 / INSIDE THE GREENHOUSE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2071,
|
||||
"titles": [
|
||||
"18 / QUIET MEDITATION ROOM"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2191,
|
||||
"titles": [
|
||||
"19 / CUSHION, CANDLES & STILLNESS"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2311,
|
||||
"titles": [
|
||||
"20 / GRAVEL ROAD & GARAGE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2431,
|
||||
"titles": [
|
||||
"21 / TESLA, SOLAR CHARGING & WORKSHOP"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2551,
|
||||
"titles": [
|
||||
"22 / FUTURE SPACES - BUILD LATER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2671,
|
||||
"titles": [
|
||||
"23 / A HOME THAT CAN CHANGE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2791,
|
||||
"titles": [
|
||||
"24 / BOOKS & A QUIET READING CORNER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 2911,
|
||||
"titles": [
|
||||
"25 / TRAINING & STRENGTH"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 3031,
|
||||
"titles": [
|
||||
"26 / OUTDOOR TABLE & MATCHA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 3151,
|
||||
"titles": [
|
||||
"27 / PLAY & FUTURE BASKETBALL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 3271,
|
||||
"titles": [
|
||||
"28 / WALKING & FOOD GARDEN"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 3391,
|
||||
"titles": [
|
||||
"29 / EASY EVERYDAY FOOD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 3511,
|
||||
"titles": [
|
||||
"30 / COPPER & MARBLE KITCHEN"
|
||||
]
|
||||
}
|
||||
],
|
||||
"render_engine": "CYCLES",
|
||||
"render_size": [
|
||||
2560,
|
||||
1440
|
||||
],
|
||||
"detail_render_enabled": true,
|
||||
"fast_excluded_from_render": true,
|
||||
"training_steps": 3,
|
||||
"play_steps": 3
|
||||
}
|
||||
30
source/samford-retreat-garage/design-checks.json
Normal file
30
source/samford-retreat-garage/design-checks.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"future_shadow_spaces": 2,
|
||||
"meditation_cushions": 1,
|
||||
"candles": 3,
|
||||
"garage_size_metres": [
|
||||
8,
|
||||
7
|
||||
],
|
||||
"cars": 1,
|
||||
"bicycles": 2,
|
||||
"rack_positions": 3,
|
||||
"rainwater_tanks": 2,
|
||||
"solar_panels": 30,
|
||||
"garden_beds": 6,
|
||||
"tour_seconds": 115,
|
||||
"chapters": 23,
|
||||
"transparent_wall_objects": 64,
|
||||
"grass_faces_cleared_from_vehicle_surfaces": 6239,
|
||||
"retained_child_beds": 0,
|
||||
"transparent_buildings": [
|
||||
"Living pavilion",
|
||||
"Parents bedroom wing",
|
||||
"Short corridor",
|
||||
"Meditation room",
|
||||
"Sauna",
|
||||
"Greenhouse",
|
||||
"Garage"
|
||||
],
|
||||
"all_23_chapters_animate": true
|
||||
}
|
||||
127
source/samford-retreat-garage/enhance_kitchen.py
Normal file
127
source/samford-retreat-garage/enhance_kitchen.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# Executed by add_daily_life.py inside the scene's construction context.
|
||||
# Replace the old simplified kitchen, retaining its position and the shared room.
|
||||
old_prefixes=('Simple timber kitchen cabinet','Flat cabinet pull','Continuous kitchen worktop','Stainless sink','Exposed kitchen tap','Tap spout','Induction cooktop','Open timber kitchen shelf','Pantry ceramic jar')
|
||||
for o in list(bpy.data.objects):
|
||||
if o.name.startswith(old_prefixes):bpy.data.objects.remove(o,do_unlink=True)
|
||||
# Pale marble: distorted fine mineral veins, soft honed reflections.
|
||||
marble=mat('Honed natural marble - ivory and warm grey veins',(.78,.76,.70),.26)
|
||||
ns=marble.node_tree.nodes;ls=marble.node_tree.links;bs=ns.get('Principled BSDF')
|
||||
coord=ns.new('ShaderNodeTexCoord');noise=ns.new('ShaderNodeTexNoise');noise.inputs['Scale'].default_value=2.8;noise.inputs['Detail'].default_value=5
|
||||
ls.new(coord.outputs['Generated'],noise.inputs['Vector'])
|
||||
scale=ns.new('ShaderNodeVectorMath');scale.operation='SCALE';scale.inputs[3].default_value=.33;ls.new(noise.outputs['Color'],scale.inputs[0])
|
||||
add=ns.new('ShaderNodeVectorMath');add.operation='ADD';ls.new(coord.outputs['Generated'],add.inputs[0]);ls.new(scale.outputs[0],add.inputs[1])
|
||||
wave=ns.new('ShaderNodeTexWave');wave.wave_type='BANDS';wave.bands_direction='DIAGONAL';wave.inputs['Scale'].default_value=3.7;wave.inputs['Distortion'].default_value=6;wave.inputs['Detail'].default_value=4;ls.new(add.outputs[0],wave.inputs['Vector'])
|
||||
ramp=ns.new('ShaderNodeValToRGB');ramp.color_ramp.elements[0].position=.055;ramp.color_ramp.elements[0].color=(.20,.21,.20,1);ramp.color_ramp.elements[1].position=.14;ramp.color_ramp.elements[1].color=(.81,.79,.73,1)
|
||||
ls.new(wave.outputs['Color'],ramp.inputs[0]);ls.new(ramp.outputs[0],bs.inputs['Base Color'])
|
||||
fine=ns.new('ShaderNodeTexNoise');fine.inputs['Scale'].default_value=170;ls.new(coord.outputs['Generated'],fine.inputs['Vector']);bump=ns.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.10;bump.inputs['Distance'].default_value=.0005;ls.new(fine.outputs['Fac'],bump.inputs['Height']);ls.new(bump.outputs[0],bs.inputs['Normal'])
|
||||
copperpot=textured('Copper cookware - brushed warm patina',(.32,.10,.035),(.72,.34,.13),(75,75,8),.26,.0006);copperpot.node_tree.nodes['Principled BSDF'].inputs['Metallic'].default_value=.94
|
||||
stainless=mat('Cookware food-contact stainless lining',(.55,.59,.61),.23,.96)
|
||||
ovenblack=mat('Oven and induction reflective smoked glass',(.009,.016,.019),.12,.25)
|
||||
mark=mat('Induction subtle etched markings',(.36,.42,.42),.45)
|
||||
display=mat('Appliance soft white display',(.72,.82,.75),.3);display.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.65,.85,.74,1);display.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=.6
|
||||
# Run of detailed cabinets, including a 600 mm oven bay.
|
||||
for i,(x,w) in enumerate([(-5.15,.72),(-4.32,.90),(-3.36,.96),(-2.38,.96),(-1.49,.78),(-.88,.42)]):
|
||||
box('Kitchen timber cabinet carcass',(x,-2.53,F+.45),(w,.64,.88),wood,.008)
|
||||
box('Kitchen recessed dark plinth',(x,-2.53,F+.075),(w-.06,.55,.13),steel,.006)
|
||||
if i==4:continue
|
||||
for row,h in enumerate([.29,.56,.79]):
|
||||
box('Kitchen flush timber drawer',(x,-2.194,F+h),(w-.018,.032,.22 if row<2 else .19),wood,.006)
|
||||
rod('Kitchen brushed drawer pull',(x-.14,-2.162,F+h+.065),(x+.14,-2.162,F+h+.065),.009,metal,sides=12)
|
||||
# Worktop parts leave a real sink opening at x=-4.32.
|
||||
for x,w in [(-5.15,.80),(-2.405,3.21)]:box('Kitchen honed marble rear worktop',(x,-2.52,F+.925),(w,.75,.045),marble,.008)
|
||||
for y,d in [(-2.80,.19),(-2.225,.16)]:box('Sink cutout marble edge',(-4.32,y,F+.925),(.86,d,.045),marble,.005)
|
||||
box('Kitchen marble low splashback',(-3.1,-2.905,F+1.09),(4.95,.025,.28),marble,.006)
|
||||
# Recessed sink basin with separate bottom and four walls.
|
||||
box('Sink recessed basin floor',(-4.32,-2.5,F+.73),(.66,.40,.025),stainless,.015)
|
||||
for x in [-4.66,-3.98]:box('Sink basin side',(x,-2.5,F+.825),(.025,.42,.20),stainless,.009)
|
||||
for y in [-2.715,-2.285]:box('Sink basin end',(-4.32,y,F+.825),(.70,.025,.20),stainless,.009)
|
||||
rod('Sink drain',(-4.32,-2.5,F+.744),(-4.32,-2.5,F+.750),.044,steel,sides=32)
|
||||
# Curved copper tap, exposed and replaceable.
|
||||
pts=[(-4.32,-2.8,F+.95),(-4.32,-2.8,F+1.26)]
|
||||
for i in range(17):
|
||||
a=pi-pi*i/16;pts.append((-4.32,-2.69+.11*cos(a),F+1.26+.11*sin(a)))
|
||||
for i,(a,b) in enumerate(zip(pts,pts[1:])):rod('Copper mixer tap '+str(i),a,b,.014,copperpot,sides=16)
|
||||
rod('Tap outlet',pts[-1],(-4.32,-2.58,F+1.20),.014,copperpot,sides=16)
|
||||
rod('Tap lever',(-4.20,-2.78,F+.95),(-4.20,-2.78,F+1.07),.01,stainless,sides=12)
|
||||
# Built-in oven, facing the living room (north).
|
||||
ox=-1.49
|
||||
box('OVEN - brushed steel surround',(ox,-2.177,F+.49),(.62,.04,.68),metal,.013)
|
||||
box('OVEN - insulated glass door',(ox,-2.144,F+.43),(.555,.032,.44),ovenblack,.015)
|
||||
box('Oven inner viewing panel',(ox,-2.125,F+.435),(.435,.006,.285),mat('Oven glass subtle interior',(.025,.035,.035),.18),.018)
|
||||
for z in [F+.37,F+.49]:box('Oven internal rack hint',(ox,-2.120,z),(.39,.004,.008),metal)
|
||||
rod('Oven tubular handle',(ox-.23,-2.085,F+.663),(ox+.23,-2.085,F+.663),.018,metal,sides=20)
|
||||
for x in [ox-.22,ox+.22]:rod('Oven handle standoff',(x,-2.14,F+.663),(x,-2.085,F+.663),.012,metal,sides=16)
|
||||
box('Oven control fascia',(ox,-2.142,F+.779),(.57,.028,.115),ovenblack,.007)
|
||||
for x in [ox-.205,ox+.205]:rod('Oven rotary dial',(x,-2.13,F+.78),(x,-2.097,F+.78),.027,metal,sides=32)
|
||||
box('Oven display',(ox,-2.121,F+.78),(.14,.004,.036),display,.002)
|
||||
for i in range(9):box('Oven bottom ventilation slot',(ox-.24+i*.06,-2.15,F+.17),(.035,.009,.011),black,.002)
|
||||
# Four-zone induction glass and etched circles.
|
||||
box('INDUCTION - four zone glass cooktop',(-1.49,-2.51,F+.961),(.70,.53,.018),ovenblack,.009)
|
||||
def horizontal_ring(name,x,y,z,r,t,material):
|
||||
ob=ring(name,(0,0,0),r,t,material);ob.rotation_euler.x=pi/2;ob.location=(x,y,z);return ob
|
||||
for x,y,r in [(-1.67,-2.64,.105),(-1.30,-2.64,.086),(-1.68,-2.40,.082),(-1.32,-2.40,.103)]:
|
||||
horizontal_ring('Induction etched cooking zone',x,y,F+.973,r,.0012,mark)
|
||||
for i in range(7):box('Induction touch slider',(-1.65+i*.05,-2.277,F+.972),(.019,.009,.002),mark,.001)
|
||||
# Slim extraction canopy with visible duct; no hidden construction language.
|
||||
box('Kitchen extraction hood',(-1.49,-2.49,F+1.68),(.82,.54,.075),metal,.01)
|
||||
for x in [-1.74,-1.24]:box('Hood warm task light',(x,-2.35,F+1.639),(.08,.12,.003),cream)
|
||||
rod('Exposed kitchen extraction duct',(-1.49,-2.60,F+1.71),(-1.49,-2.60,F+2.33),.105,metal,sides=48)
|
||||
# Timber shelving away from hob and exposed copper cookware rail.
|
||||
box('Kitchen upper hardwood shelf',(-3.96,-2.72,F+1.84),(2.77,.32,.05),wood,.008)
|
||||
rod('Copper cookware hanging rail',(-5.1,-2.57,F+1.52),(-2.75,-2.57,F+1.52),.014,copperpot,sides=20)
|
||||
for x in [-5.04,-2.81]:rod('Cookware rail wall bracket',(x,-2.89,F+1.52),(x,-2.57,F+1.52),.012,metal,sides=16)
|
||||
# Hollow copper vessels with stainless lining; induction-compatible base concept.
|
||||
def vessel(n,x,y,z,r,h):
|
||||
N=64;profile=[(0,0),(r*.83,0),(r,.025),(r,h),(r-.012,h+.003),(r-.016,.036),(0,.036)]
|
||||
vs=[(x+rr*cos(i*2*pi/N),y+rr*sin(i*2*pi/N),z+zz) for rr,zz in profile for i in range(N)]
|
||||
fs=[]
|
||||
for j in range(len(profile)-1):
|
||||
for i in range(N):fs.append((j*N+i,j*N+(i+1)%N,(j+1)*N+(i+1)%N,(j+1)*N+i))
|
||||
ob=mesh(n,vs,fs,copperpot);ob.data.materials.append(stainless)
|
||||
for p in ob.data.polygons:p.use_smooth=True;p.material_index=1 if p.index//N>=3 else 0
|
||||
rod(n+' induction compatible base concept',(x,y,z-.003),(x,y,z+.004),r*.85,stainless,sides=64)
|
||||
horizontal_ring(n+' rolled rim',x,y,z+h,r-.005,.006,stainless)
|
||||
return ob
|
||||
vessel('COPPER STOCKPOT',-2.91,-2.50,F+.954,.18,.235)
|
||||
for x in [-3.13,-2.69]:
|
||||
rod('Stockpot handle',(x,-2.57,F+1.12),(x,-2.43,F+1.12),.012,metal,sides=16)
|
||||
for y in [-2.57,-2.43]:rod('Stockpot handle fixing',(x,y,F+1.12),(x+(.055 if x<-2.9 else -.055),y,F+1.12),.009,metal,sides=12)
|
||||
vessel('COPPER SAUCEPAN',-1.32,-2.40,F+.977,.09,.09)
|
||||
rod('Saucepan handle',(-1.22,-2.40,F+1.05),(-.95,-2.40,F+1.09),.012,metal,sides=20)
|
||||
vessel('COPPER FRYING PAN',-3.45,.02,F+.965,.155,.037)
|
||||
rod('Frying pan long handle',(-3.29,.02,F+1.0),(-3.0,.02,F+1.03),.013,metal,sides=20)
|
||||
# Marble-topped island: central prep surface clear of the dining table.
|
||||
ix,iy=-3.8,.10
|
||||
box('ISLAND - warm timber cabinet body',(ix,iy,F+.44),(2.05,.83,.88),wood,.012)
|
||||
box('ISLAND - recessed plinth',(ix,iy,F+.06),(1.93,.73,.12),steel,.007)
|
||||
box('ISLAND - honed marble countertop',(ix,iy,F+.94),(2.2,.95,.06),marble,.012)
|
||||
for x in [ix-.68,ix,ix+.68]:
|
||||
for z in [.30,.69]:
|
||||
box('Island drawer front',(x,iy-.425,F+z),(.657,.028,.36),wood,.006)
|
||||
rod('Island drawer pull',(x-.14,iy-.448,F+z+.09),(x+.14,iy-.448,F+z+.09),.009,metal,sides=16)
|
||||
for i in range(29):box('Island north face timber slat',(ix-.98+i*.07,iy+.425,F+.49),(.042,.023,.75),woodz,.004)
|
||||
box('Island end grain chopping board',(-4.37,.14,F+.985),(.45,.34,.03),wood,.015)
|
||||
# Tea crockery and pantry objects add scale without filling the work surface.
|
||||
for x in [-5.03,-4.67,-4.30]:
|
||||
vessel('Shelf ceramic storage vessel',x,-2.71,F+1.87,.085,.18)
|
||||
for ob in list(col.objects):
|
||||
if ob.name.startswith('Shelf ceramic storage vessel') and ob.type=='MESH':
|
||||
for slot in range(len(ob.data.materials)):ob.data.materials[slot]=cream
|
||||
# Rebuild only the old furniture proxy so replaced kitchen blocks disappear in fast view.
|
||||
furniture=bpy.data.collections.get('10 Furniture');fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
|
||||
oldproxy=bpy.data.objects.get('FAST | 10 Furniture')
|
||||
if oldproxy and furniture:
|
||||
vs=[];fs=[];indices=[];materials=[];mapping={}
|
||||
for ob in furniture.objects:
|
||||
if ob.type!='MESH' or ob.hide_render:continue
|
||||
remap=[]
|
||||
for m in ob.data.materials:
|
||||
if m not in mapping:mapping[m]=len(materials);materials.append(m)
|
||||
remap.append(mapping[m])
|
||||
world=Matrix.LocRotScale(ob.location.copy(),ob.rotation_euler.to_quaternion(),ob.scale.copy());off=len(vs);vs.extend(tuple(world@v.co) for v in ob.data.vertices)
|
||||
for poly in ob.data.polygons:fs.append(tuple(off+j for j in poly.vertices));indices.append(remap[poly.material_index])
|
||||
me=bpy.data.meshes.new('Furniture after kitchen replacement');me.from_pydata(vs,[],fs);me.update()
|
||||
for m in materials:me.materials.append(m)
|
||||
me.polygons.foreach_set('material_index',indices);oldproxy.data=me
|
||||
kitchen_checks={'oven':1,'induction_zones':4,'copper_stockpot':1,'copper_saucepan':1,'copper_frying_pan':1,'marble_island_m':[2.2,.95],'island_rear_work_aisle_m':1.67,'old_simplified_kitchen_removed':True,'fast_furniture_refreshed':bool(oldproxy)}
|
||||
s['Kitchen design']='Honed marble island, detailed timber drawers, oven, four-zone induction, extraction, hollow copper cookware with stainless food-contact lining and an induction-compatible base concept.'
|
||||
71
source/samford-retreat-garage/export_meditation_unreal.py
Normal file
71
source/samford-retreat-garage/export_meditation_unreal.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import bpy,os,re,json,bmesh
|
||||
from mathutils import Vector,Matrix
|
||||
OUT=os.path.abspath(os.path.join(os.path.dirname(__file__),'../samford-meditation-unreal'));os.makedirs(OUT+'/Textures',exist_ok=True)
|
||||
source=bpy.context.scene
|
||||
detail=bpy.data.collections.get('DETAIL - full geometry for renders')
|
||||
if detail:detail.hide_viewport=False
|
||||
source.frame_set(2220);bpy.context.view_layer.update()
|
||||
origin=Vector((-11,12,1.12))
|
||||
objects=list(bpy.data.collections['23 NOW - Quiet meditation room'].objects)
|
||||
objects += [o for o in bpy.data.collections['20 Spacious site connections'].objects if o.name.startswith('Meditation room')]
|
||||
objects=[o for o in objects if o.type=='MESH' and not o.name.startswith(('Meditation covered walk','Meditation walk'))]
|
||||
deps=bpy.context.evaluated_depsgraph_get()
|
||||
new=bpy.data.scenes.new('Meditation room - Unreal transfer');new.unit_settings.system='METRIC';new.unit_settings.scale_length=1
|
||||
manifest={'source':'Samford solarpunk homestead','origin_blender_metres':list(origin),'room_metres':[4.5,4.8],'objects':[],'materials':{},'camera_blender_metres':[1.7,2.0,1.88],'target_blender_metres':[-.5,-.5,.43],'camera_lens_mm':25,'render_status':'Prepared for Unreal; no Unreal render has been produced by this exporter.'}
|
||||
new_objects=[];used=[]
|
||||
for i,o in enumerate(objects):
|
||||
ev=o.evaluated_get(deps);me=bpy.data.meshes.new_from_object(ev,preserve_all_data_layers=True,depsgraph=deps)
|
||||
me.transform(Matrix.Translation(-origin)@o.matrix_world)
|
||||
ob=bpy.data.objects.new('SM_%03d_'%i+re.sub('[^a-zA-Z0-9_]','_',o.name),me);new.collection.objects.link(ob);new_objects.append(ob)
|
||||
for mat in me.materials:
|
||||
if mat and mat not in used:used.append(mat)
|
||||
manifest['objects'].append({'name':ob.name,'source':o.name})
|
||||
bpy.context.window.scene=new
|
||||
# Unwrap the small room assets; source procedural shaders remain in the original file.
|
||||
for o in new_objects:
|
||||
bpy.ops.object.select_all(action='DESELECT');o.select_set(True);bpy.context.view_layer.objects.active=o
|
||||
bpy.ops.object.mode_set(mode='EDIT');bpy.ops.mesh.select_all(action='SELECT');bpy.ops.uv.smart_project(island_margin=.025);bpy.ops.object.mode_set(mode='OBJECT')
|
||||
new.render.engine='CYCLES';new.cycles.device='CPU';new.cycles.samples=1;new.render.bake.margin=4
|
||||
bpy.ops.object.select_all(action='DESELECT');bpy.ops.mesh.primitive_plane_add(size=2);tile=bpy.context.object;tile.name='Temporary material baking tile'
|
||||
replacement={}
|
||||
for i,m in enumerate(used):
|
||||
key='M_%02d_'%i+re.sub('[^a-zA-Z0-9_]','_',m.name)
|
||||
p=next((n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED'),None) if m.use_nodes else None
|
||||
base=list(m.diffuse_color);rough=float(p.inputs['Roughness'].default_value) if p else .7;metal=float(p.inputs['Metallic'].default_value) if p else 0
|
||||
record={'source':m.name,'base_color':base,'roughness':rough,'metallic':metal,'emission':0,'transmission':0}
|
||||
if p:
|
||||
record['emission']=float(p.inputs['Emission Strength'].default_value)
|
||||
record['transmission']=float(p.inputs['Transmission Weight'].default_value)
|
||||
exported=bpy.data.materials.new(key);exported.use_nodes=True;ep=exported.node_tree.nodes.get('Principled BSDF');ep.inputs['Base Color'].default_value=base;ep.inputs['Roughness'].default_value=rough;ep.inputs['Metallic'].default_value=metal
|
||||
if p and p.inputs['Base Color'].is_linked:
|
||||
bake=m.copy();tile.data.materials.clear();tile.data.materials.append(bake);nodes=bake.node_tree.nodes;links=bake.node_tree.links;bp=next(n for n in nodes if n.type=='BSDF_PRINCIPLED');output=next(n for n in nodes if n.type=='OUTPUT_MATERIAL')
|
||||
im=bpy.data.images.new(key+'_BaseColor',width=512,height=512);im.filepath_raw=OUT+'/Textures/'+key+'_BaseColor.png';im.file_format='PNG'
|
||||
target=nodes.new('ShaderNodeTexImage');target.image=im;nodes.active=target
|
||||
emission=nodes.new('ShaderNodeEmission');links.new(bp.inputs['Base Color'].links[0].from_socket,emission.inputs['Color']);links.new(emission.outputs[0],output.inputs['Surface'])
|
||||
bpy.ops.object.select_all(action='DESELECT');tile.select_set(True);bpy.context.view_layer.objects.active=tile;bpy.ops.object.bake(type='EMIT');im.save()
|
||||
tex=exported.node_tree.nodes.new('ShaderNodeTexImage');tex.image=im;exported.node_tree.links.new(tex.outputs['Color'],ep.inputs['Base Color']);record['base_color_texture']='Textures/'+key+'_BaseColor.png'
|
||||
if bp.inputs['Normal'].is_linked:
|
||||
normal_image=bpy.data.images.new(key+'_Normal',width=512,height=512)
|
||||
normal_image.colorspace_settings.name='Non-Color';normal_image.filepath_raw=OUT+'/Textures/'+key+'_Normal.png';normal_image.file_format='PNG'
|
||||
target.image=normal_image;nodes.active=target;links.new(bp.outputs['BSDF'],output.inputs['Surface'])
|
||||
bpy.ops.object.bake(type='NORMAL');normal_image.save()
|
||||
normal_tex=exported.node_tree.nodes.new('ShaderNodeTexImage');normal_tex.image=normal_image
|
||||
normal_node=exported.node_tree.nodes.new('ShaderNodeNormalMap');exported.node_tree.links.new(normal_tex.outputs['Color'],normal_node.inputs['Color']);exported.node_tree.links.new(normal_node.outputs['Normal'],ep.inputs['Normal'])
|
||||
record['normal_texture']='Textures/'+key+'_Normal.png'
|
||||
record['normal_convention']='OpenGL +Y'
|
||||
bpy.data.materials.remove(bake)
|
||||
replacement[m]=exported;manifest['materials'][key]=record
|
||||
print('MATERIAL_READY '+key,flush=True)
|
||||
for o in new_objects:
|
||||
for slot in o.material_slots:
|
||||
if slot.material in replacement:slot.material=replacement[slot.material]
|
||||
bpy.data.objects.remove(tile,do_unlink=True)
|
||||
# This deliberately keeps the viewing face open for the simple room render.
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for o in new_objects:o.select_set(True)
|
||||
bpy.context.view_layer.objects.active=new_objects[0]
|
||||
bpy.ops.export_scene.fbx(filepath=OUT+'/MeditationRoom.fbx',use_selection=True,object_types={'MESH'},use_mesh_modifiers=False,add_leaf_bones=False,bake_anim=False,axis_forward='-Y',axis_up='Z',path_mode='RELATIVE')
|
||||
bpy.ops.export_scene.gltf(filepath=OUT+'/MeditationRoom.glb',export_format='GLB',use_selection=True,export_animations=False)
|
||||
json.dump(manifest,open(OUT+'/meditation-room-manifest.json','w'),indent=2)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/meditation-room-transfer.blend')
|
||||
print('MEDITATION_EXPORT_READY '+str(len(new_objects))+' meshes, '+str(len(used))+' materials',flush=True)
|
||||
60
source/samford-retreat-garage/finish_and_render.py
Normal file
60
source/samford-retreat-garage/finish_and_render.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import bpy, bmesh, math, json, os
|
||||
from mathutils import Vector
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
|
||||
# Clear grass from the new vehicle surfaces and garage interior.
|
||||
route=[(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)]
|
||||
segments=[]
|
||||
for a,b in zip(route,route[1:]):
|
||||
dx,dy=b[0]-a[0],b[1]-a[1]; segments.append((a[0],a[1],dx,dy,dx*dx+dy*dy))
|
||||
def clear(x,y):
|
||||
if -7.2<x<1.2 and -22.7<y<-15.3:return True
|
||||
if -7.05<x<5.05 and -15.55<y<-9.45:return True
|
||||
for ax,ay,dx,dy,l2 in segments:
|
||||
t=max(0,min(1,((x-ax)*dx+(y-ay)*dy)/l2))
|
||||
if (x-ax-t*dx)**2+(y-ay-t*dy)**2<2.08**2:return True
|
||||
return False
|
||||
removed=0
|
||||
for o in s.objects:
|
||||
if o.type=='MESH' and o.name.startswith('Native grasses'):
|
||||
bm=bmesh.new(); bm.from_mesh(o.data); faces=[]
|
||||
for f in bm.faces:
|
||||
p=o.matrix_world@f.calc_center_median()
|
||||
if clear(p.x,p.y):faces.append(f)
|
||||
removed+=len(faces); bmesh.ops.delete(bm,geom=faces,context='FACES'); bm.to_mesh(o.data); bm.free()
|
||||
# This broad panel is the vehicle entrance, not a permanent wall across the car bay.
|
||||
o=bpy.data.objects.get('Garage north front panel')
|
||||
if o:o.name='Garage 3.7m wide sectional vehicle door'; o['Operation']='Concept upward-opening vehicle door; transparent for presentation.'
|
||||
o=bpy.data.objects.get('Garage roller door concept')
|
||||
if o:o.name='Garage north service infill panel'
|
||||
# Distinct static overview for inspecting the driveway without changing the Play camera.
|
||||
camdata=bpy.data.cameras.new('VIEW - Road garage and retreat'); cam=bpy.data.objects.new(camdata.name,camdata); s.collection.objects.link(cam)
|
||||
cam.location=(47,40,47); cam.rotation_euler=(Vector((0,-7,1))-cam.location).to_track_quat('-Z','Y').to_euler(); camdata.lens=38; camdata.clip_end=450
|
||||
tour=bpy.data.objects['PLAY TOUR camera']; s.camera=tour; s.frame_set(1)
|
||||
report=json.load(open(os.path.join(OUT,'design-checks.json'))); report['grass_faces_cleared_from_vehicle_surfaces']=removed
|
||||
report['retained_child_beds']=sum(o.name.startswith('Child') and ('bed' in o.name or 'mattress' in o.name) for o in s.objects)
|
||||
report['transparent_buildings']=['Living pavilion','Parents bedroom wing','Short corridor','Meditation room','Sauna','Greenhouse','Garage']
|
||||
assert report['retained_child_beds']==0 and report['candles']==3
|
||||
chapters=json.load(open(os.path.join(OUT,'tour-chapters.json')))
|
||||
for ch in chapters:
|
||||
s.frame_set(ch['start']); a=tour.location.copy(); s.frame_set(ch['end']); b=tour.location.copy(); assert (b-a).length>.01
|
||||
report['all_23_chapters_animate']=True; json.dump(report,open(os.path.join(OUT,'design-checks.json'),'w'),indent=2)
|
||||
s.camera=tour; s.frame_set(1)
|
||||
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='SOLID'; area.spaces.active.shading.color_type='MATERIAL'; area.spaces.active.overlay.show_overlays=False
|
||||
s.render.engine='CYCLES'; s.render.resolution_percentage=100; s.cycles.samples=48; s.cycles.use_denoising=True; s.cycles.adaptive_threshold=.035
|
||||
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.render.filepath=os.path.join(OUT,'retreat-garage-overview.png')
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
|
||||
# Captions are useful during playback, but photographs use clean architectural views.
|
||||
titles=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
|
||||
for o in titles:o.hide_render=True
|
||||
s.camera=cam; bpy.ops.render.render(write_still=True); print('OVERVIEW_RENDERED',flush=True)
|
||||
s.camera=tour; s.frame_set(2220); s.render.filepath=os.path.join(OUT,'meditation-room.png'); bpy.ops.render.render(write_still=True); print('MEDITATION_RENDERED',flush=True)
|
||||
s.frame_set(2460); s.render.filepath=os.path.join(OUT,'garage-workstation-bikes.png'); bpy.ops.render.render(write_still=True); print('GARAGE_RENDERED',flush=True)
|
||||
for o in titles:o.hide_render=False
|
||||
s.camera=tour; s.frame_set(1); s.render.filepath=os.path.join(OUT,'retreat-garage-overview.png')
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
|
||||
print('FINAL_RETREAT_READY_TO_PLAY',flush=True)
|
||||
11
source/samford-retreat-garage/garden-flow-checks.json
Normal file
11
source/samford-retreat-garage/garden-flow-checks.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"deleted_objects": 143,
|
||||
"moved_objects": 1655,
|
||||
"flower_beds": 13,
|
||||
"planted_clumps": 221,
|
||||
"flowering_shrubs": 8,
|
||||
"smooth_routes": 13,
|
||||
"table_tennis_removed": true,
|
||||
"future_pavilions": 2,
|
||||
"tour_chapters": 30
|
||||
}
|
||||
5118
source/samford-retreat-garage/garden-flow-layout.json
Normal file
5118
source/samford-retreat-garage/garden-flow-layout.json
Normal file
File diff suppressed because it is too large
Load Diff
24
source/samford-retreat-garage/garden-flow-verification.json
Normal file
24
source/samford-retreat-garage/garden-flow-verification.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"no_table_tennis": true,
|
||||
"garage_center": [
|
||||
0.0,
|
||||
-19.0
|
||||
],
|
||||
"meditation_center": [
|
||||
-11.8,
|
||||
12.0
|
||||
],
|
||||
"caption_issues": [],
|
||||
"tour_chapters": 30,
|
||||
"rainwater_tanks": 2,
|
||||
"kitchen_preserved": true,
|
||||
"flower_beds": 13,
|
||||
"render_size": [
|
||||
2560,
|
||||
1440,
|
||||
100
|
||||
],
|
||||
"fast_excluded_from_render": true,
|
||||
"pavilion_path_intersections": [],
|
||||
"solar_tree_clearance": true
|
||||
}
|
||||
25
source/samford-retreat-garage/meditation_finish.py
Normal file
25
source/samford-retreat-garage/meditation_finish.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import bpy,os
|
||||
from mathutils import Vector
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene; c=bpy.data.collections['23 NOW - Quiet meditation room']
|
||||
# Keep the entrance clear and support the low stone candle shelf on matching stone feet.
|
||||
for o in c.objects:
|
||||
if o.name.startswith(('Low stone candle shelf','Stone candle holder','Meditation candle','Small candle flame','Soft candle glow')):o.location.x-=1.4
|
||||
for x in [-12.9,-11.9]:
|
||||
bpy.ops.mesh.primitive_cube_add(size=1,location=(x,10.36,1.235)); o=bpy.context.object; o.name='Low candle shelf stone foot'; o.dimensions=(.14,.30,.23); bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
|
||||
for col in list(o.users_collection):col.objects.unlink(o)
|
||||
c.objects.link(o); o.data.materials.append(bpy.data.materials['Split sandstone facing - mineral grain']); mod=o.modifiers.new('Soft stone edge','BEVEL'); mod.width=.008; mod.segments=2
|
||||
cam=bpy.data.objects['PLAY TOUR camera']
|
||||
for f,p in [(2161,(-9.3,14.0,3.0)),(2280,(-9.6,13.5,2.8))]:
|
||||
cam.location=p; cam.rotation_euler=(Vector((-11.5,11.5,1.55))-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=25; cam.data.keyframe_insert('lens',frame=f)
|
||||
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
|
||||
if o.type=='FONT' and o.name.startswith('19 /'):half=36/25; o.location=(-half*.91,half*9/16*.84,-2)
|
||||
s.frame_set(1); s.camera=cam
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
|
||||
titles=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
|
||||
for o in titles:o.hide_render=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'; s.cycles.samples=40; s.frame_set(2220); s.render.filepath=os.path.join(OUT,'meditation-room.png'); bpy.ops.render.render(write_still=True)
|
||||
for o in titles:o.hide_render=False
|
||||
s.frame_set(1); s.render.filepath=os.path.join(OUT,'retreat-garage-overview.png'); bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
|
||||
print('MEDITATION_FINAL_READY',flush=True)
|
||||
100
source/samford-retreat-garage/optimize_playback.py
Normal file
100
source/samford-retreat-garage/optimize_playback.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import bpy,json,time,statistics,os
|
||||
from mathutils import Vector
|
||||
OUT=os.path.dirname(os.path.abspath(__file__))
|
||||
s=bpy.context.scene
|
||||
original=list(s.objects)
|
||||
visible=[o for o in original if o.visible_get()]
|
||||
render_flags={o:o.hide_render for o in original}
|
||||
render_settings=(s.render.engine,s.render.resolution_percentage,s.render.filepath)
|
||||
def preview(tag,objects):
|
||||
for o in s.objects:o.hide_render=o not in objects
|
||||
s.render.engine='BLENDER_WORKBENCH';s.render.resolution_percentage=40
|
||||
s.display.shading.light='STUDIO';s.display.shading.color_type='MATERIAL'
|
||||
s.display.shading.show_shadows=False;s.display.shading.show_cavity=False
|
||||
s.display.shading.show_specular_highlight=False
|
||||
times=[]
|
||||
for f in [1,60,2460]:
|
||||
s.frame_set(f);s.render.filepath=os.path.join(OUT,tag+('-garage' if f==2460 else '')+'.png')
|
||||
t=time.perf_counter();bpy.ops.render.render(write_still=f!=60);times.append(time.perf_counter()-t)
|
||||
return times
|
||||
base_objects={o for o in visible if o.display_type not in {'WIRE','BOUNDS'}}
|
||||
baseline=preview('performance-before',base_objects)
|
||||
for o,flag in render_flags.items():o.hide_render=flag
|
||||
s.render.engine=render_settings[0]
|
||||
s.frame_set(1)
|
||||
fast=bpy.data.collections.new('00 FAST TOUR - lightweight viewport');s.collection.children.link(fast)
|
||||
fast.hide_render=True
|
||||
def combined(name,objects):
|
||||
verts=[];faces=[];indices=[];smooth=[];materials=[];mat_map={}
|
||||
for o in objects:
|
||||
me=o.data;is_leaf='individual canopy leaves' in o.name
|
||||
mi=[]
|
||||
for mat in me.materials:
|
||||
if mat not in mat_map:
|
||||
mat_map[mat]=len(materials);materials.append(mat)
|
||||
mi.append(mat_map[mat])
|
||||
def material_idx(i):return mi[i] if mi and i<len(mi) else 0
|
||||
if is_leaf:
|
||||
# Keep one complete leaf in four. Flatten its four tiny triangles
|
||||
# into one quad and enlarge slightly to preserve canopy coverage.
|
||||
assert len(me.vertices)%5==0 and len(me.polygons)==len(me.vertices)//5*4
|
||||
for k in range(0,len(me.vertices),20):
|
||||
c=sum((me.vertices[k+j].co for j in range(4)),Vector())/4
|
||||
off=len(verts)
|
||||
verts.extend(tuple(o.matrix_world@(c+(me.vertices[k+j].co-c)*1.55)) for j in range(4))
|
||||
faces.append((off,off+1,off+2,off+3));indices.append(material_idx(me.polygons[k//5*4].material_index));smooth.append(False)
|
||||
else:
|
||||
off=len(verts);verts.extend(tuple(o.matrix_world@v.co) for v in me.vertices)
|
||||
faces.extend(tuple(off+i for i in p.vertices) for p in me.polygons)
|
||||
indices.extend(material_idx(p.material_index) for p in me.polygons)
|
||||
smooth.extend(p.use_smooth for p in me.polygons)
|
||||
if not faces:return
|
||||
me=bpy.data.meshes.new(name);me.from_pydata(verts,[],faces);me.update()
|
||||
for m in materials:me.materials.append(m)
|
||||
me.polygons.foreach_set('material_index',indices);me.polygons.foreach_set('use_smooth',smooth)
|
||||
ob=bpy.data.objects.new(name,me);fast.objects.link(ob)
|
||||
ob['Purpose']='Lightweight static tour display. Original editable geometry is retained in the DETAIL collection and used for renders.'
|
||||
ob['Source objects']=len(objects)
|
||||
return ob
|
||||
seen=set()
|
||||
for col in list(bpy.data.collections):
|
||||
if col==fast:continue
|
||||
obs=[o for o in col.objects if o in base_objects and o.type=='MESH' and o not in seen and not o.animation_data and not o.parent]
|
||||
if not obs:continue
|
||||
combined('FAST | '+col.name,obs);seen.update(obs)
|
||||
# Originals remain fully editable, with all materials and render visibility intact.
|
||||
# Globally disabling their collection affects only viewport evaluation.
|
||||
detail=bpy.data.collections.new('DETAIL - full geometry for renders');s.collection.children.link(detail)
|
||||
for col in list(s.collection.children):
|
||||
if col in {fast,detail} or col.name.startswith('22 PLAY'):continue
|
||||
s.collection.children.unlink(col);detail.children.link(col)
|
||||
detail.hide_viewport=True
|
||||
for screen in bpy.data.screens:
|
||||
for a in screen.areas:
|
||||
if a.type=='VIEW_3D':
|
||||
sp=a.spaces.active;sp.shading.type='SOLID';sp.shading.light='STUDIO';sp.shading.color_type='MATERIAL'
|
||||
sp.shading.show_cavity=False;sp.shading.show_shadows=False;sp.shading.show_specular_highlight=False
|
||||
sp.overlay.show_overlays=False;sp.region_3d.view_perspective='CAMERA'
|
||||
s.sync_mode='FRAME_DROP';s.cycles.preview_samples=8
|
||||
s['Viewport performance']='FAST TOUR provides simplified static meshes for navigation. Detailed geometry is retained under DETAIL, disabled only in the viewport. Final Cycles renders automatically use DETAIL and omit FAST TOUR.'
|
||||
s['Editing full detail']='In the Outliner filter menu show Disable in Viewports (monitor icon). Enable DETAIL and disable FAST TOUR to edit the full model. Return to FAST TOUR for playback.'
|
||||
fast_objects=set(fast.objects)|{o for o in original if o.type in {'CAMERA','FONT'} and o in base_objects}
|
||||
# Make preview-only proxies available to the independent Workbench test.
|
||||
fast.hide_render=False
|
||||
optimized=preview('performance-after',fast_objects)
|
||||
fast.hide_render=True
|
||||
for o,flag in render_flags.items():o.hide_render=flag
|
||||
for o in fast.objects:o.hide_render=False
|
||||
s.render.engine=render_settings[0];s.render.resolution_percentage=render_settings[1];s.render.filepath=render_settings[2]
|
||||
s.frame_set(1);bpy.context.view_layer.update()
|
||||
times=[]
|
||||
for f in range(1,97):
|
||||
t=time.perf_counter();s.frame_set(f);bpy.context.view_layer.update();times.append(time.perf_counter()-t)
|
||||
now=[o for o in s.objects if o.visible_get()]
|
||||
report={'before_visible_objects':len(visible),'after_visible_objects':len(now),'before_visible_faces':sum(len(o.data.polygons) for o in visible if o.type=='MESH'),'after_visible_faces':sum(len(o.data.polygons) for o in now if o.type=='MESH'),'workbench_before_seconds':baseline,'workbench_after_seconds':optimized,'after_frame_update_ms_median':statistics.median(times)*1000,'original_render_visibility_preserved':all(o.hide_render==v for o,v in render_flags.items()),'original_objects_preserved':all(o.name in s.objects for o in original),'fast_geometry_excluded_from_renders':fast.hide_render,'detailed_geometry_disabled_only_in_viewport':detail.hide_viewport and not detail.hide_render}
|
||||
assert report['original_render_visibility_preserved'] and report['original_objects_preserved'] and report['fast_geometry_excluded_from_renders'] and report['detailed_geometry_disabled_only_in_viewport']
|
||||
json.dump(report,open(os.path.join(OUT,'performance-checks.json'),'w'),indent=2)
|
||||
s.frame_set(1);s.camera=bpy.data.objects['PLAY TOUR camera']
|
||||
filename='samford-solarpunk-smooth-tour.blend' if bpy.data.objects.get('TESLA MODEL Y - pearl white flowing body') else 'samford-retreat-smooth-tour.blend'
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,filename))
|
||||
print('PERFORMANCE_RESULT '+json.dumps(report),flush=True)
|
||||
26
source/samford-retreat-garage/performance-checks.json
Normal file
26
source/samford-retreat-garage/performance-checks.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"before_visible_objects": 4090,
|
||||
"after_visible_objects": 47,
|
||||
"before_visible_faces": 1911296,
|
||||
"after_visible_faces": 197676,
|
||||
"workbench_before_seconds": [
|
||||
1.7875650000005407,
|
||||
1.0031288749996747,
|
||||
1.2839702920009586
|
||||
],
|
||||
"workbench_after_seconds": [
|
||||
0.21899866700005077,
|
||||
0.16925074999926437,
|
||||
0.17747099999905913
|
||||
],
|
||||
"after_frame_update_ms_median": 0.17172900061268592,
|
||||
"original_render_visibility_preserved": true,
|
||||
"original_objects_preserved": true,
|
||||
"fast_geometry_excluded_from_renders": true,
|
||||
"detailed_geometry_disabled_only_in_viewport": true,
|
||||
"all_23_tour_chapters_verified_after_reload": true,
|
||||
"tesla_present": true,
|
||||
"garage_solar_modules": 8,
|
||||
"ev_charger_present": true,
|
||||
"full_detail_final_render": "Render flags and geometry retained; checked with a Cycles garage render."
|
||||
}
|
||||
8
source/samford-retreat-garage/reading-corner-checks.json
Normal file
8
source/samford-retreat-garage/reading-corner-checks.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"books": 163,
|
||||
"bookshelf_bays": 2,
|
||||
"reading_chairs": 1,
|
||||
"tour_chapters": 24,
|
||||
"tour_seconds": 120,
|
||||
"fast_proxy_objects_added": 1
|
||||
}
|
||||
234
source/samford-retreat-garage/refine_garden_flow.py
Normal file
234
source/samford-retreat-garage/refine_garden_flow.py
Normal file
@@ -0,0 +1,234 @@
|
||||
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)
|
||||
269
source/samford-retreat-garage/revise_scene.py
Normal file
269
source/samford-retreat-garage/revise_scene.py
Normal file
@@ -0,0 +1,269 @@
|
||||
import bpy, math, os, json, random, bmesh
|
||||
from mathutils import Vector
|
||||
from math import sin,cos,pi
|
||||
random.seed(602)
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
|
||||
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
|
||||
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
|
||||
# Keep one room as a retreat; the other two become undeveloped shadow footprints.
|
||||
med=bpy.data.collections['23 FUTURE - Child 1 private room']; med.name='23 NOW - Quiet meditation room'; med['Purpose']='Simple meditation retreat now; adaptable room in future.'
|
||||
for name in ['24 FUTURE - Child 2 private room','25 FUTURE - Child 3 private room','26 Children courtyard and covered links']:
|
||||
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(med.objects):
|
||||
if any(t in o.name for t in ['single bed','mattress','blanket','pillow','desk','stool','wardrobe','pinboard','display shelf']): bpy.data.objects.remove(o,do_unlink=True)
|
||||
elif o.type=='FONT': o.data.body='QUIET / MEDITATION'; o.name='Meditation room entry sign'
|
||||
else: o.name=o.name.replace('Child 1','Meditation').replace('Child room','Meditation room')
|
||||
# Remove roof-junction leftovers from the two deleted rooms, which were stored separately.
|
||||
for o in list(bpy.data.collections['20 Spacious site connections'].objects):
|
||||
if o.name.startswith('Child room'):
|
||||
if center(o).x>-6: bpy.data.objects.remove(o,do_unlink=True)
|
||||
else: o.name=o.name.replace('Child room','Meditation room')
|
||||
COL={'med':med}
|
||||
for key,name in [('future','24 LATER - Shadow spaces only'),('road','27 Gravel access road and arrival'),('garage','28 Garage and workbench'),('car','29 Car and bicycles'),('cutaway','30 VIEW - Transparent building sides')]:
|
||||
c=bpy.data.collections.new(name); s.collection.children.link(c); COL[key]=c
|
||||
active='med'; 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']; linen=bpy.data.materials['Undyed flax linen']; gravel=bpy.data.materials['Loose local gravel - fine stones']; F=1.12
|
||||
zafu=textured('Meditation cushion charcoal linen',(.085,.08,.063),(.19,.17,.13),(100,100,100),.95,.003)
|
||||
tatami=textured('Woven natural meditation mat',(.33,.28,.17),(.54,.47,.31),(120,8,120),.94,.004)
|
||||
wax=mat('Beeswax candle',(.70,.50,.22),.63); flame=mat('Small warm candle flame',(.95,.43,.08),.5); p=flame.node_tree.nodes['Principled BSDF']; p.inputs['Emission Color'].default_value=(1,.34,.04,1); p.inputs['Emission Strength'].default_value=5
|
||||
box('Simple woven tatami mat',(-11,12,F+.025),(2.05,2.2,.05),tatami,.035)
|
||||
box('Meditation square zabuton',(-11,12.05,F+.075),(.95,.95,.10),linen,.10)
|
||||
rod('ROUND MEDITATION CUSHION',(-11,12.05,F+.12),(-11,12.05,F+.28),.34,zafu,sides=64)
|
||||
box('Low stone candle shelf',(-11,10.36,F+.29),(1.35,.38,.12),stone,.022)
|
||||
for x,h in [(-11.42,.16),(-11,.22),(-10.58,.13)]:
|
||||
rod('Stone candle holder',(x,10.36,F+.35),(x,10.36,F+.39),.10,stone,sides=24)
|
||||
rod('Meditation candle',(x,10.36,F+.39),(x,10.36,F+.39+h),.041,wax,sides=24)
|
||||
rod('Meditation candle wick',(x,10.36,F+.39+h),(x,10.36,F+.415+h),.003,steel,sides=8)
|
||||
rod('Small candle flame',(x,10.36,F+.40+h),(x,10.36,F+.46+h),.014,flame,r2=.001,sides=12)
|
||||
d=bpy.data.lights.new('Soft candle glow','POINT'); d.energy=1.5; d.color=(1,.39,.10); d.shadow_soft_size=.06; ob=bpy.data.objects.new(d.name,d); COL[active].objects.link(ob); ob.location=(x,10.36,F+.44+h)
|
||||
box('Meditation low timber bench',(-12.6,11.9,F+.37),(.45,1.45,.065),wood,.02)
|
||||
for y in [11.34,12.46]: box('Meditation bench leg',(-12.6,y,F+.18),(.36,.05,.36),wood)
|
||||
# A minimal shaded walk goes only to the actual meditation room.
|
||||
for cx,cy,w,dep in [(-3,7.2,1.2,3.6),(-7,9,8,1.2)]:
|
||||
box('Meditation covered walk deck',(cx,cy,F-.07),(w,dep,.14),wood,.008)
|
||||
box('Meditation covered walk roof',(cx,cy,F+2.55),(w+.14,dep+.14,.055),roofmat,.008)
|
||||
for x,y in [(cx-w/2+.1,cy-dep/2+.1),(cx+w/2-.1,cy+dep/2-.1)]:
|
||||
g=ground(x,y); box('Meditation walk timber post',(x,y,(g+F+2.52)/2),(.085,.085,F+2.52-g),woodz,.006)
|
||||
def ghost(name,color,opacity):
|
||||
m=mat(name,color,.8); ns=m.node_tree.nodes; ls=m.node_tree.links; p=ns.get('Principled BSDF'); out=ns.get('Material Output'); tr=ns.new('ShaderNodeBsdfTransparent'); mix=ns.new('ShaderNodeMixShader'); mix.inputs[0].default_value=opacity; ls.new(tr.outputs[0],mix.inputs[1]); ls.new(p.outputs[0],mix.inputs[2]); ls.new(mix.outputs[0],out.inputs['Surface']); m.diffuse_color=(*color,opacity); return m
|
||||
active='future'; gm=ghost('Future shadow - faint sage outline',(.49,.59,.50),.32); shade=ghost('Future shadow - translucent footprint',(.20,.26,.21),.10)
|
||||
for idx,(cx,cy) in enumerate([(-3,17),(7,15)]):
|
||||
xs,xe=cx-2.25,cx+2.25; ys,yn=cy-2.4,cy+2.4
|
||||
for x in [xs,xe]:
|
||||
for y,h in [(ys,2.75),(yn,3.1)]: beam('Future space %d | shadow edge'%(idx+1),(x,y,F),(x,y,F+h),.028,.028,gm)
|
||||
beam('Future shadow roof slope',(x,ys,F+2.75),(x,yn,F+3.1),.028,.028,gm)
|
||||
for y,h in [(ys,2.75),(yn,3.1)]:
|
||||
beam('Future shadow roof line',(xs,y,F+h),(xe,y,F+h),.028,.028,gm)
|
||||
beam('Future shadow floor line',(xs,y,F),(xe,y,F),.028,.028,gm)
|
||||
for x in [xs,xe]: beam('Future shadow floor side',(x,ys,F),(x,yn,F),.028,.028,gm)
|
||||
o=box('Future space %d | ground shadow'%(idx+1),(cx,cy,ground(cx,cy)+.03),(4.5,4.8,.01),shade); o['Status']='Unbuilt reserve: no furniture, solid walls or occupied room.'
|
||||
# Road enters from the southeast and approaches a turning apron in front of the garage.
|
||||
active='road'; roadpoints=[(36,-40),(28,-33),(22,-27),(8,-26),(5,-22),(5,-13),(1,-12),(-3,-12)]
|
||||
def road(name,points,width):
|
||||
vs=[]; fs=[]
|
||||
for aa,bb in zip(points,points[1:]):
|
||||
a,b=Vector(aa),Vector(bb); delta=b-a; perp=Vector((-delta.y,delta.x)).normalized()*width/2; n=max(3,int(delta.length/.45))
|
||||
start=len(vs)
|
||||
for i in range(n):
|
||||
p=a+delta*i/(n-1)
|
||||
for q in [p-perp,p+perp]: vs.append((q.x,q.y,ground(q.x,q.y)+.065))
|
||||
for i in range(n-1): k=start+i*2; fs.append((k,k+1,k+3,k+2))
|
||||
return mesh(name,vs,fs,gravel)
|
||||
road('VEHICLE ACCESS - 4m gravel driveway',roadpoints,4)
|
||||
# Gravel apron draped over local terrain.
|
||||
vs=[]; fs=[]
|
||||
for j in range(9):
|
||||
for i in range(17):
|
||||
x=-7+i*.75; y=-15.5+j*.75; vs.append((x,y,ground(x,y)+.07))
|
||||
for j in range(8):
|
||||
for i in range(16): k=j*17+i; fs.append((k,k+1,k+18,k+17))
|
||||
mesh('Gravel turning and garage arrival apron',vs,fs,gravel)
|
||||
road('Garage to house pedestrian path',[(-3,-10),(-2,-6),(-5.5,-4),(-6.5,-2.8)],1.1)
|
||||
for x,y in [(34,-37),(29,-32),(22,-27),(6,-24),(5,-18),(3,-10)]:
|
||||
g=ground(x,y); box('Simple timber driveway edge marker',(x+2.35,y,g+.37),(.09,.09,.74),woodz,.01)
|
||||
# Detached garage: one car bay, workshop bench and a clear cycle-storage zone.
|
||||
active='garage'; gx,gy=-3,-19; GF=.62; w,dep=8,7; xs,xe=gx-4,gx+4; ys,yn=gy-3.5,gy+3.5
|
||||
slab=box('GARAGE - 8m x 7m concrete floor',(gx,gy,GF-.11),(w,dep,.22),bpy.data.materials['Warm oxide polished concrete - fine aggregate'],.018)
|
||||
for x in [xs+.15,xe-.15]:
|
||||
for y in [ys+.15,yn-.15]:
|
||||
g=ground(x,y); box('Garage concrete foundation pier',(x,y,(g+GF-.2)/2),(.40,.40,max(.08,GF-.2-g)),stone,.015)
|
||||
box('Garage back clay wall',(gx,ys,GF+1.44),(w,.17,2.88),clay)
|
||||
box('Garage west weatherboard wall',(xs,gy,GF+1.51),(.17,dep,3.02),weather)
|
||||
box('Garage east workshop wall',(xe,gy,GF+1.51),(.17,dep,3.02),clay)
|
||||
# The north viewing facade retains a closed-door design underneath the transparent presentation.
|
||||
box('Garage north front panel',(-5.0,yn,GF+1.43),(3.7,.15,2.86),weather)
|
||||
box('Garage north pedestrian door',(.24,yn,GF+1.16),(1.16,.07,2.32),wood,.009)
|
||||
box('Garage north narrow infill',(-.66,yn,GF+1.43),(.61,.15,2.86),weather)
|
||||
box('Garage north upper header',(gx,yn,GF+3.03),(w,.17,.36),wood)
|
||||
box('Garage roller door concept',(-2.14,yn,GF+1.30),(1.9,.09,2.6),roofmat,.008)
|
||||
# The car bay uses the broad west section: transparent front panels expose its parked car.
|
||||
for x in [xs,xe,-2.8]:
|
||||
for y,h in [(ys,2.95),(yn,3.3)]: box('Garage exposed timber post',(x,y,GF+h/2),(.14,.14,h),woodz,.005)
|
||||
for i in range(12):
|
||||
x=xs+i*w/11; beam('Garage exposed rafter',(x,ys-.30,GF+2.94),(x,yn+.30,GF+3.33),.07,.20,wood)
|
||||
ro=box('Garage pale mono-pitch roof',(gx,gy,GF+3.22),(8.7,7.7,.075),roofmat,.01); ro.rotation_euler.x=math.atan(.35/7)
|
||||
box('Garage rear roof gutter',(gx,ys-.40,GF+3.03),(8.7,.14,.10),roofmat,.006)
|
||||
# Entrance ramp blends the vehicle bay floor down to the apron.
|
||||
vs=[(x,y,z) for x in [xs+.2,gx+.3] for y,z in [(yn,GF),(yn+2.7,ground(x,yn+2.7)+.07)]]
|
||||
mesh('Garage shallow entry ramp',vs,[(0,2,3,1)],gravel)
|
||||
benchx,benchy=-.47,-21.5
|
||||
box('GARAGE WORKSTATION - timber workbench',(benchx,benchy,GF+.92),(2.50,.76,.09),wood,.015)
|
||||
for x in [benchx-1.07,benchx+1.07]:
|
||||
for y in [benchy-.28,benchy+.28]: box('Workbench steel leg',(x,y,GF+.44),(.055,.055,.88),steel,.005)
|
||||
box('Workbench lower shelf',(benchx,benchy,GF+.22),(2.4,.66,.045),wood)
|
||||
box('Workshop toolboard',(benchx,ys+.12,GF+1.85),(2.5,.07,.98),wood,.008)
|
||||
for i in range(8):
|
||||
x=benchx-1.05+i*.30; rod('Workbench tool hook',(x,ys+.17,GF+1.9),(x,ys+.25,GF+1.9),.009,steel)
|
||||
box('Workshop hanging tool handle',(x,ys+.23,GF+1.68),(.03,.03,.27),woodz,.005)
|
||||
box('Workshop tool head',(x,ys+.23,GF+1.89),(.10,.045,.06),steel,.008)
|
||||
box('Workbench small vice',(benchx-.85,benchy+.34,GF+1.04),(.25,.20,.17),steel,.02)
|
||||
rod('Vice turning handle',(benchx-1.00,benchy+.48,GF+1.03),(benchx-.69,benchy+.48,GF+1.03),.012,steel)
|
||||
black=mat('Garage rubber and dark tools',(.025,.03,.028),.78); alu=mat('Garage brushed aluminium',(.40,.43,.43),.30,.8)
|
||||
box('Workbench compact display',(benchx+.5,benchy-.19,GF+1.22),(.56,.04,.34),black,.015)
|
||||
box('Workbench keyboard',(benchx+.5,benchy+.12,GF+.98),(.34,.12,.018),black,.007)
|
||||
box('Workbench stool',(benchx,benchy+1.1,GF+.51),(.42,.42,.06),wood,.025)
|
||||
for x in [benchx-.16,benchx+.16]:
|
||||
for y in [benchy+.94,benchy+1.26]: box('Workbench stool leg',(x,y,GF+.24),(.035,.035,.48),steel)
|
||||
light=bpy.data.lights.new('Garage warm work light','AREA'); light.energy=320; light.size=3.4; light.color=(1,.85,.66); ob=bpy.data.objects.new(light.name,light); COL[active].objects.link(ob); ob.location=(-2,-19,GF+2.7)
|
||||
# One generic family wagon, parked nose toward the transparent front.
|
||||
active='car'; paint=mat('Family wagon muted green',(.20,.28,.25),.32,.4); autoglass=mat('Vehicle tinted glass',(.035,.07,.085),.20,.3); lamps=mat('Vehicle headlamp lens',(.70,.76,.73),.22)
|
||||
cx,cy=-4.7,-19.1
|
||||
box('ONE FAMILY CAR - wagon body',(cx,cy,GF+.70),(1.86,4.35,.56),paint,.16)
|
||||
box('Car lower sill',(cx,cy,GF+.46),(1.77,4.12,.22),black,.08)
|
||||
vs=[(cx+x,cy+y,GF+z) for x,y,z in [(-.81,-1.25,.96),(.81,-1.25,.96),(.81,1.08,.96),(-.81,1.08,.96),(-.69,-.97,1.65),(.69,-.97,1.65),(.69,.64,1.65),(-.69,.64,1.65)]]
|
||||
mesh('Family wagon glazed cabin',vs,[(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7),(4,5,6,7)],autoglass)
|
||||
box('Car roof',(cx,cy-.16,GF+1.67),(1.43,1.65,.075),paint,.05)
|
||||
for x in [cx-.87,cx+.87]:
|
||||
for y in [cy-1.40,cy+1.40]:
|
||||
rod('Car rubber tyre',(x-.11,y,GF+.36),(x+.11,y,GF+.36),.35,black,sides=48)
|
||||
rod('Car alloy wheel',(x-.12,y,GF+.36),(x+.12,y,GF+.36),.23,alu,sides=32)
|
||||
box('Car side mirror',(x+(.13 if x>cx else -.13),cy+.55,GF+1.08),(.20,.24,.12),paint,.05)
|
||||
box('Car door handle',(x+(.07 if x>cx else -.07),cy,GF+.93),(.025,.17,.027),alu,.01)
|
||||
for x in [cx-.61,cx+.61]: box('Car front headlamp',(x,cy+2.14,GF+.82),(.45,.04,.15),lamps,.045)
|
||||
box('Car front bumper',(cx,cy+2.18,GF+.50),(1.68,.05,.15),black,.05)
|
||||
box('Car blank number plate',(cx,cy+2.22,GF+.65),(.42,.018,.10),lamps,.01)
|
||||
# Two bicycles in a three-position floor rack beside the car, clear of the bench.
|
||||
bikemat=[mat('Bicycle ochre frame',(.53,.31,.10),.4,.45),mat('Bicycle blue green frame',(.10,.30,.31),.4,.45)]
|
||||
def hoop(name,x,y,z,r,tube,material):
|
||||
n=56; vs=[]; fs=[]
|
||||
for i in range(n):
|
||||
a=2*pi*i/n
|
||||
for j in range(8):
|
||||
b=2*pi*j/8; rr=r+tube*cos(b); vs.append((x+tube*sin(b),y+rr*cos(a),z+rr*sin(a)))
|
||||
for i in range(n):
|
||||
for j in range(8): fs.append((i*8+j,i*8+(j+1)%8,((i+1)%n)*8+(j+1)%8,((i+1)%n)*8+j))
|
||||
o=mesh(name,vs,fs,material)
|
||||
for p in o.data.polygons:p.use_smooth=True
|
||||
for idx,bx in enumerate([-.65,.25]):
|
||||
by=-18.0; z=GF+.36; m=bikemat[idx]; rear=Vector((bx,by-.66,z)); front=Vector((bx,by+.66,z)); crank=Vector((bx,by-.04,z+.05)); saddle=Vector((bx,by-.27,z+.63)); head=Vector((bx,by+.43,z+.59))
|
||||
for wheel in [rear,front]:
|
||||
hoop('Bicycle %d tyre'%(idx+1),wheel.x,wheel.y,wheel.z,.34,.022,black)
|
||||
hoop('Bicycle aluminium rim',wheel.x,wheel.y,wheel.z,.302,.010,alu)
|
||||
for i in range(12):
|
||||
a=2*pi*i/12; rod('Bicycle spoke',wheel,wheel+Vector((0,.302*cos(a),.302*sin(a))),.003,alu,sides=5)
|
||||
for a,b in [(rear,saddle),(saddle,crank),(crank,rear),(saddle,head),(crank,head),(head,front)]: rod('Bicycle frame tube',a,b,.022,m,sides=12)
|
||||
box('Bicycle saddle',(bx,saddle.y,z+.69),(.19,.27,.05),black,.04)
|
||||
rod('Bicycle handlebar',(bx-.26,head.y+.04,z+.81),(bx+.26,head.y+.04,z+.81),.014,alu,sides=12)
|
||||
rod('Bicycle stem',head,(bx,head.y+.04,z+.81),.015,alu,sides=12)
|
||||
rod('Bicycle crank',(bx-.15,crank.y,crank.z),(bx+.15,crank.y,crank.z),.014,steel,sides=12)
|
||||
for x in [-.65,.25,-1.55]:
|
||||
for xx in [x-.075,x+.075]: rod('BIKE RACK wheel guide',(xx,-17.47,GF+.03),(xx,-16.97,GF+.03),.025,steel,sides=12)
|
||||
rod('Bike rack upright',(x,-16.98,GF+.03),(x,-16.98,GF+.5),.025,steel,sides=12)
|
||||
box('Bike rack shared base',(-.65,-17.22,GF+.03),(2.45,.58,.055),steel,.01)
|
||||
# Non-destructive presentation cutaways: retain wall geometry but use near-transparent skin materials.
|
||||
cutawaycache={}; cutobjects=[]
|
||||
def transparent_side(o):
|
||||
if o.type!='MESH': return
|
||||
original=[]
|
||||
for slot in o.material_slots:
|
||||
m=slot.material
|
||||
if not m: continue
|
||||
original.append(m.name)
|
||||
if m.name not in cutawaycache:
|
||||
col=tuple(m.diffuse_color[:3]); cutawaycache[m.name]=ghost('VIEW transparent / '+m.name,col,.055)
|
||||
slot.material=cutawaycache[m.name]
|
||||
o['Original wall materials']=json.dumps(original); o['Presentation only']='Transparent viewing facade. Original solid-wall material names preserved here.'
|
||||
# Hide filled wall faces in fast solid viewport, leaving structural posts and opening frames visible.
|
||||
o.hide_set(True); cutobjects.append(o.name)
|
||||
for cc in list(o.users_collection): cc.objects.unlink(o)
|
||||
COL['cutaway'].objects.link(o)
|
||||
for name in ['01 Living pavilion 12 x 6 m','02 Sleeping pavilion 8 x 5 m','23 NOW - Quiet meditation room','16 Oval Finnish sauna','06 Kitchen garden','28 Garage and workbench','20 Spacious site connections']:
|
||||
coll=bpy.data.collections.get(name)
|
||||
if not coll: continue
|
||||
for o in list(coll.objects):
|
||||
n=o.name; p=center(o); use=False
|
||||
if name.startswith('01'): use=n.startswith('Sliding glass pane')
|
||||
elif name.startswith('02'): use=n.startswith(('Bedroom north wall','Bedroom north lintel','Sliding glass pane'))
|
||||
elif name.startswith('23'): use=n.startswith(('Meditation room north clay wall','Meditation room north window infill','Meditation room north openable glazing'))
|
||||
elif name.startswith('16'): use=n.startswith(('Sauna front timber cheek','Sauna entrance oval infill','Sauna continuous tempered-glass','Sauna outward-opening'))
|
||||
elif name.startswith('06'): use=('Greenhouse end glazing' in n and p.y>-6) or ('Greenhouse side glazing' in n and p.x>24)
|
||||
elif name.startswith('28'): use=n.startswith(('Garage north front panel','Garage north pedestrian door','Garage north narrow infill','Garage roller door concept'))
|
||||
elif name.startswith('20'): use=n.startswith('Corridor north wall') or (n.startswith('Meditation room north wall roof junction'))
|
||||
if use: transparent_side(o)
|
||||
# Fix all newly created solid normals, including stock helpers inherited from the first model.
|
||||
for coll in COL.values():
|
||||
for o in coll.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()
|
||||
# Replace obsolete children-room chapters with the meditation retreat, garage and future shadows.
|
||||
cam=bpy.data.objects['PLAY TOUR camera']; data=cam.data; tour=bpy.data.collections['22 PLAY - guided camera tour']; caption=bpy.data.materials['Tour captions ivory']
|
||||
chapters=json.load(open(os.path.join(OUT,'../samford-spacious-tour/tour-chapters.json')))[:17]
|
||||
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 m in list(s.timeline_markers):
|
||||
if m.frame>=2041: s.timeline_markers.remove(m)
|
||||
newshots={
|
||||
17:('18 / QUIET MEDITATION ROOM',(-8.4,16.5,3.8),(-9,15.8,3.4),(-11,11.7,1.65),(-11,11.5,1.65),32),
|
||||
18:('19 / CUSHION, CANDLES & STILLNESS',(-9.6,13.5,2.7),(-10,13.1,2.55),(-11,11.3,1.45),(-11,11.0,1.45),27),
|
||||
19:('20 / GRAVEL ROAD & GARAGE',(17,-4,10),(13,-7,8),(-3,-18,1.5),(-3,-18,1.5),36),
|
||||
20:('21 / CAR, WORKBENCH & BIKE RACKS',(-.5,-11,4.0),(-1.4,-12,3.6),(-2.3,-19,1.5),(-2.3,-19.4,1.5),30),
|
||||
21:('22 / FUTURE SPACES - BUILD LATER',(19,28,13),(16,25,11),(2,16,1.5),(2,16,1.5),37),
|
||||
22:('23 / A HOME THAT CAN CHANGE',(44,64,47),(49,69,50),(-3,-4,1),(-3,-4,1),40),
|
||||
}
|
||||
for i,(title,a,b,ta,tb,lens) in newshots.items():
|
||||
start=i*120+1; end=(i+1)*120
|
||||
for f,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=f); cam.keyframe_insert('rotation_euler',frame=f); data.lens=lens; data.keyframe_insert('lens',frame=f)
|
||||
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'
|
||||
s.frame_end=2760; s.frame_set(1); s.camera=cam; s.sync_mode='FRAME_DROP'
|
||||
s['PLAY THE TOUR']='Space over 3D view or Timeline Play. Shift+Left Arrow restarts. 115 seconds / 23 chapters / 24 fps.'
|
||||
s['Current design']='One meditation retreat with cushion and candles; two unbuilt shadow spaces. Detached 8x7m garage with car, workbench and bicycle racks. Southeast gravel driveway. Transparent viewing facades are presentation-only.'
|
||||
s['Future additions']='Two faint unbuilt shadow rooms replace solid child rooms. Meditation cabin can be adapted later.'
|
||||
s['Transparent walls']='Collection 30 stores near-transparent wall skins. Filled faces hidden only in solid viewport. Structural frames stay visible. Original materials recorded on each object.'
|
||||
txt=bpy.data.texts['START HERE - PRESS PLAY']; txt.clear(); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Current design']+'\n\n'+s['Transparent walls'])
|
||||
names=[o.name for o in s.objects]
|
||||
report={'future_shadow_spaces':2,'meditation_cushions':sum(n.startswith('ROUND MEDITATION CUSHION') for n in names),'candles':sum(n.startswith('Meditation candle') and 'wick' not in n for n in names),'garage_size_metres':[8,7],'cars':sum(n.startswith('ONE FAMILY CAR') for n in names),'bicycles':2,'rack_positions':3,'rainwater_tanks':sum(n.startswith('2000 mm') for n in names),'solar_panels':sum(n.startswith('PV panel glass and cells') for n in names),'garden_beds':sum(n.startswith('Raised vegetable bed soil') for n in names),'tour_seconds':115,'chapters':len(chapters),'transparent_wall_objects':len(cutobjects)}
|
||||
assert report['meditation_cushions']==1 and report['cars']==1 and report['rainwater_tanks']==2 and report['solar_panels']==30 and report['garden_beds']==6 and report['transparent_wall_objects']>20
|
||||
json.dump(report,open(os.path.join(OUT,'design-checks.json'),'w'),indent=2); json.dump(chapters,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
|
||||
s.render.engine='CYCLES'; s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100; s.cycles.samples=40; s.cycles.use_denoising=True
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-retreat-garage-tour.blend'))
|
||||
# Fast chapter checks are separate from the saved model's render configuration.
|
||||
for o in COL['cutaway'].objects:o.hide_render=True
|
||||
for o in s.objects:
|
||||
if o.name=='Subtle atmosphere over hillside':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 [0,6,9,17,18,19,20,21,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('RETREAT_GARAGE_DRAFT_COMPLETE',flush=True)
|
||||
139
source/samford-retreat-garage/solarpunk_tesla.py
Normal file
139
source/samford-retreat-garage/solarpunk_tesla.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import bpy,math,random,os,json,bmesh
|
||||
from mathutils import Vector
|
||||
from math import sin,cos,pi
|
||||
OUT=os.path.dirname(os.path.abspath(__file__));s=bpy.context.scene;random.seed(41)
|
||||
COL={'car':bpy.data.collections['29 Car and bicycles'],'garage':bpy.data.collections['28 Garage and workbench']};active='car'
|
||||
src=open(OUT+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
|
||||
# Restore outward cube winding for newly added geometry.
|
||||
cubefs=[tuple(reversed(f)) for f in cubefs]
|
||||
for o in list(COL['car'].objects):
|
||||
if o.name.startswith(('Car ','Family wagon','ONE FAMILY CAR')):bpy.data.objects.remove(o,do_unlink=True)
|
||||
paint=mat('Tesla pearl white multi-coat',(.82,.84,.81),.23,.25);paint.node_tree.nodes['Principled BSDF'].inputs['Coat Weight'].default_value=.55
|
||||
glass=mat('Tesla smoked panoramic glass',(.026,.043,.055),.14,.35)
|
||||
rubber=mat('Tesla tyre rubber',(.018,.020,.021),.82)
|
||||
black=mat('Tesla satin black trim',(.025,.029,.031),.4,.4)
|
||||
silver=mat('Tesla machined wheel accents',(.52,.57,.59),.22,.9)
|
||||
led=mat('Tesla LED white',(.8,.89,.91),.17);p=led.node_tree.nodes['Principled BSDF'];p.inputs['Emission Color'].default_value=(.8,.9,1,1);p.inputs['Emission Strength'].default_value=.8
|
||||
red=mat('Tesla rear light red',(.42,.009,.012),.25)
|
||||
cx,cy,GF=-4.7,-19.1,.62
|
||||
def world(p):return (cx+p[0],cy+p[1],GF+p[2])
|
||||
def path(name,points,r,material):
|
||||
cu=bpy.data.curves.new(name,'CURVE');cu.dimensions='3D';cu.resolution_u=8;cu.bevel_depth=r;cu.bevel_resolution=2
|
||||
sp=cu.splines.new('BEZIER');sp.bezier_points.add(len(points)-1)
|
||||
for b,p in zip(sp.bezier_points,points):b.co=p;b.handle_left_type='AUTO';b.handle_right_type='AUTO'
|
||||
ob=bpy.data.objects.new(name,cu);COL[active].objects.link(ob);cu.materials.append(material)
|
||||
return ob
|
||||
def apply_smooth(ob,level=1):
|
||||
bm=bmesh.new();bm.from_mesh(ob.data);bmesh.ops.recalc_face_normals(bm,faces=bm.faces);bm.to_mesh(ob.data);bm.free()
|
||||
bpy.context.view_layer.objects.active=ob;ob.select_set(True)
|
||||
if level:
|
||||
m=ob.modifiers.new('Baked flowing body surface','SUBSURF');m.levels=level;bpy.ops.object.modifier_apply(modifier=m.name)
|
||||
for p in ob.data.polygons:p.use_smooth=True
|
||||
ob.select_set(False)
|
||||
# 2020-24 Model Y inspired body, 4.75m long, with open wheel arches.
|
||||
ys=[-2.375,-2.34,-2.22,-2.02,-1.85,-1.70,-1.55,-1.445,-1.33,-1.17,-1.03,-.88,-.6,0,.55,.86,1.03,1.18,1.32,1.445,1.57,1.72,1.86,2.05,2.25,2.34,2.375]
|
||||
vs=[];fs=[]
|
||||
for y in ys:
|
||||
w=.965-.14*(abs(y)/2.375)**6
|
||||
top=.99-.23*max(0,(y-.8)/1.575)+.05*max(0,(-y-1.3)/1.075)
|
||||
arch=max([.28]+[.37+math.sqrt(max(0,.405**2-(y-wy)**2)) for wy in [-1.445,1.445] if abs(y-wy)<.405])
|
||||
ring=[(-w*.86,.25),(-w,arch),(-w,top-.13),(-w*.91,top),(-w*.52,top+.035),(0,top+.045),(w*.52,top+.035),(w*.91,top),(w,top-.13),(w,arch),(w*.86,.25),(0,.23)]
|
||||
vs.extend(world((x,y,z)) for x,z in ring)
|
||||
for j in range(len(ys)-1):
|
||||
for i in range(12):a=j*12+i;b=j*12+(i+1)%12;fs.append((a,b,b+12,a+12))
|
||||
fs.extend([tuple(reversed(range(12))),tuple(range((len(ys)-1)*12,len(ys)*12))])
|
||||
body=mesh('TESLA MODEL Y - pearl white flowing body',vs,fs,paint);apply_smooth(body,2)
|
||||
# Arched glass roof, windscreen and rear hatch form one continuous dark canopy.
|
||||
sections=[(-2.04,.68,1.02,1.10),(-1.8,.73,1.02,1.28),(-1.28,.75,1.03,1.53),(-.8,.74,1.04,1.62),(-.22,.74,1.04,1.64),(.32,.74,1.03,1.60),(.75,.78,1.00,1.40),(1.12,.81,.99,1.06)]
|
||||
vs=[];fs=[]
|
||||
for y,w,b,t in sections:
|
||||
for x,z in [(-w,b),(-w*.92,b+(t-b)*.7),(-w*.73,t-.025),(0,t),(w*.73,t-.025),(w*.92,b+(t-b)*.7),(w,b)]:vs.append(world((x,y,z)))
|
||||
for j in range(len(sections)-1):
|
||||
for i in range(6):a=j*7+i;fs.append((a,a+1,a+8,a+7))
|
||||
canopy=mesh('Tesla sweeping panoramic glass roof and windows',vs,fs,glass);apply_smooth(canopy,2)
|
||||
for side in [-1,1]:
|
||||
path('Tesla continuous window surround',[world((side*w,y,b+.025)) for y,w,b,t in sections],.022,black)
|
||||
path('Tesla roof shoulder pillar',[world((side*w*.79,y,t-.075)) for y,w,b,t in sections[1:-1]],.023,paint)
|
||||
for y,top in [(.69,1.40),(-.32,1.59),(-1.3,1.45)]:
|
||||
path('Tesla window pillar',[world((side*.81,y,1.025)),world((side*.65,y,top))],.025,black if y<.1 else paint)
|
||||
for y in [-.65,.55]:box('Tesla flush black door handle',world((side*.963,y,.925)),(.016,.17,.025),black,.006)
|
||||
path('Tesla door shut line',[world((side*.963,-.30,.36)),world((side*.965,-.32,.75)),world((side*.91,-.34,1.01))],.0035,black)
|
||||
box('Tesla side mirror',world((side*1.02,.70,1.10)),(.24,.18,.10),black,.045)
|
||||
for wy in [-1.445,1.445]:
|
||||
x=side*.935
|
||||
tyre=rod('Tesla 19 inch tyre',world((x-.12,wy,.36)),world((x+.12,wy,.36)),.355,rubber,sides=64)
|
||||
outer=side*1.061
|
||||
rod('Tesla dark aero wheel',world((outer,wy,.36)),world((outer+side*.008,wy,.36)),.248,black,sides=48)
|
||||
rod('Tesla wheel centre',world((outer+side*.01,wy,.36)),world((outer+side*.019,wy,.36)),.061,silver,sides=24)
|
||||
for k in range(5):
|
||||
a=k*2*pi/5
|
||||
path('Tesla turbine spoke',[world((outer+side*.011,wy+.07*cos(a),.36+.07*sin(a))),world((outer+side*.014,wy+.22*cos(a+.22),.36+.22*sin(a+.22)))],.022,silver)
|
||||
path('Tesla narrow swept headlamp',[world((side*.47,2.30,.79)),world((side*.69,2.23,.82)),world((side*.82,2.08,.84))],.029,led)
|
||||
path('Tesla red rear lamp',[world((side*.41,-2.34,1.00)),world((side*.67,-2.33,.99)),world((side*.83,-2.22,.94))],.028,red)
|
||||
box('Tesla lower front air intake',world((0,2.351,.40)),(1.24,.026,.09),black,.035)
|
||||
# Recognisable Tesla T emblem on the nose; no imported heavy vehicle asset.
|
||||
path('Tesla T badge upper arc',[world((-.08,2.17,.828)),world((0,2.20,.824)),world((.08,2.17,.828))],.006,black)
|
||||
path('Tesla T badge stem',[world((0,2.19,.826)),world((0,2.29,.812))],.006,black)
|
||||
def label(name,body,pos,rot,size,material):
|
||||
cu=bpy.data.curves.new(name,'FONT');cu.body=body;cu.size=size;cu.align_x='CENTER';cu.extrude=.0005
|
||||
ob=bpy.data.objects.new(name,cu);COL[active].objects.link(ob);ob.location=pos;ob.rotation_euler=rot;cu.materials.append(material);return ob
|
||||
box('Tesla front number plate',world((0,2.382,.60)),(.41,.018,.105),paint,.012)
|
||||
label('Tesla MODEL Y plate','MODEL Y',world((0,2.394,.573)),(pi/2,0,pi),.061,black)
|
||||
label('Tesla rear lettering','T E S L A',world((0,-2.38,.92)),(pi/2,0,0),.078,silver)
|
||||
active='garage'
|
||||
wood=bpy.data.materials['Unpainted hardwood - long X grain'];steel=bpy.data.materials['Reusable dark steel bolts and shoes']
|
||||
solar=mat('Garage solar blue black cells',(.023,.065,.098),.23,.65);green=mat('Solar charge indicator',(.1,.65,.3),.3)
|
||||
green.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.1,1,.3,1);green.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=1.3
|
||||
box('SOLAR CHARGING - timber backboard',(-6.88,-20.8,GF+1.45),(.07,.52,.89),wood,.025)
|
||||
box('Tesla wall connector',(-6.79,-20.8,GF+1.45),(.16,.24,.47),paint,.075)
|
||||
box('Tesla charger black face',(-6.70,-20.8,GF+1.46),(.016,.20,.37),black,.035)
|
||||
box('Solar charging green status',(-6.685,-20.8,GF+1.62),(.009,.13,.014),green,.005)
|
||||
path('EV cable to left rear charge port',[(-6.73,-20.8,GF+1.20),(-6.59,-20.8,GF+.60),(-6.34,-20.8,GF+.20),(-5.96,-20.92,GF+.24),(-5.71,-21.08,GF+.76),(-5.66,-21.08,GF+.93)],.014,black)
|
||||
box('Tesla plugged charge connector',(-5.67,-21.08,GF+.94),(.09,.07,.07),black,.022)
|
||||
for x in [-5.9,-4.0,-2.1,-.2]:
|
||||
for y in [-20.9,-18.5]:
|
||||
z=GF+3.22+.05*(y+19)+.105
|
||||
for name,dims,offset,material in [('Garage solar aluminium frame',(1.78,2.14,.05),0,silver),('Garage photovoltaic module',(1.70,2.06,.018),.034,solar)]:
|
||||
ob=box(name,(x,y,z+offset),dims,material,.009);ob.rotation_euler.x=math.atan(.05)
|
||||
for i in range(1,6):
|
||||
ob=box('Solar cell division',(x-.85+i*1.7/6,y,z+.046),(.005,2.02,.004),silver);ob.rotation_euler.x=math.atan(.05)
|
||||
for j in range(1,10):
|
||||
yy=y-1.03+j*2.06/10;box('Solar cell row',(x,yy,z+.046+.05*(yy-y)),(1.69,.005,.004),silver)
|
||||
# A planted trellis softens the workshop's east wall without narrowing car access.
|
||||
soil=bpy.data.materials['Damp brown garden soil'];leaves=[bpy.data.materials['Leaf '+str(i)] for i in range(5)]
|
||||
for x in [1.23,2.03]:box('Garage edible planter timber side',(x,-19,.79),(.055,3.6,.43),wood,.015)
|
||||
for y in [-20.8,-17.2]:box('Garage edible planter end',(1.63,y,.79),(.85,.055,.43),wood,.015)
|
||||
box('Garage edible planter soil',(1.63,-19,.94),(.75,3.5,.06),soil)
|
||||
for y in [-20.7,-19.8,-18.9,-18,-17.3]:box('Garage climbing garden upright',(1.14,y,1.88),(.04,.04,2.35),wood)
|
||||
for z in [1.2,1.65,2.10,2.55,2.95]:box('Garage climbing garden cross rail',(1.14,-19,z),(.04,3.6,.035),wood)
|
||||
lv=[];lf=[];lm=[]
|
||||
for i in range(240):
|
||||
y=random.uniform(-20.7,-17.3);z=random.uniform(1.03,2.88);x=1.17+random.uniform(-.05,.20)
|
||||
if random.random()<.40:z=random.uniform(.96,1.32);x=random.uniform(1.24,2.0)
|
||||
a=random.uniform(0,2*pi);length=random.uniform(.07,.14);c=Vector((x,y,z));long=Vector((.1,cos(a),sin(a)))*length;wide=Vector((.3,-sin(a),cos(a)))*length*.48;j=len(lv)
|
||||
lv.extend(tuple(p) for p in [c-long,c+wide,c+long,c-wide]);lf.append((j,j+1,j+2,j+3));lm.append(random.randrange(5))
|
||||
o=mesh('Garage climbing edible greenery',lv,lf)
|
||||
for m in leaves:o.data.materials.append(m)
|
||||
for p,i in zip(o.data.polygons,lm):p.material_index=i
|
||||
# Bake added curve and text geometry so the fast tour includes every feature.
|
||||
for col in COL.values():
|
||||
for o in list(col.objects):
|
||||
if o.type in {'CURVE','FONT'}:
|
||||
bpy.ops.object.select_all(action='DESELECT');o.select_set(True);bpy.context.view_layer.objects.active=o;bpy.ops.object.convert(target='MESH');o.select_set(False)
|
||||
terrain=bpy.data.objects['Continuous gently north falling hillside']
|
||||
for v in terrain.data.vertices:
|
||||
p=terrain.matrix_world@v.co
|
||||
if -7.2<p.x<1.2 and -22.7<p.y<-15.3 and p.z>GF-.23:
|
||||
p.z=GF-.23;v.co=terrain.matrix_world.inverted()@p
|
||||
terrain.data.update()
|
||||
cam=bpy.data.objects['PLAY TOUR camera']
|
||||
for frame,pos,target in [(2401,(-6.5,-12.8,3.00),(-3.7,-19.6,1.45)),(2520,(-6.25,-14.45,2.50),(-3.8,-19.9,1.44))]:
|
||||
s.frame_set(frame);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);cam.data.lens=26;cam.data.keyframe_insert('lens',frame=frame)
|
||||
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
|
||||
if o.type=='FONT' and '21 /' in o.data.body:o.data.body='21 / TESLA, SOLAR CHARGING & WORKSHOP'
|
||||
s['Design direction']='Solarpunk homestead: natural timber and clay, food growing, solar-powered electric mobility, rainwater capture, repair workshop and adaptable modular rooms.'
|
||||
s['Tesla reference']='Simplified 2020-2024 Model Y inspired concept model, approximately 4.75 m long. https://www.tesla.com/ownersmanual/2020_2024_modely/en_cn/Owners_Manual.pdf'
|
||||
s['Energy concept']='Eight extra garage solar modules and conceptual EV wall connector. No electrical capacity or charging performance is simulated.'
|
||||
s.frame_set(1);s.camera=cam
|
||||
bpy.ops.wm.save_as_mainfile(filepath=OUT+'/samford-solarpunk-detailed.blend')
|
||||
print('SOLARPUNK_TESLA_BUILT',flush=True)
|
||||
182
source/samford-retreat-garage/tour-chapters-daily-life.json
Normal file
182
source/samford-retreat-garage/tour-chapters-daily-life.json
Normal file
@@ -0,0 +1,182 @@
|
||||
[
|
||||
{
|
||||
"title": "01 / THE SPACIOUS HOMESTEAD",
|
||||
"start": 1,
|
||||
"end": 120,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "02 / LIVING PAVILION & VERANDA",
|
||||
"start": 121,
|
||||
"end": 240,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
|
||||
"start": 241,
|
||||
"end": 360,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "04 / DINING & OPEN LIVING",
|
||||
"start": 361,
|
||||
"end": 480,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "05 / ONE SERVER - LOCAL AI",
|
||||
"start": 481,
|
||||
"end": 600,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
|
||||
"start": 601,
|
||||
"end": 720,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "07 / ADJACENT BEDROOM WING",
|
||||
"start": 721,
|
||||
"end": 840,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "08 / ENCLOSED BEDROOM",
|
||||
"start": 841,
|
||||
"end": 960,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
|
||||
"start": 961,
|
||||
"end": 1080,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
|
||||
"start": 1081,
|
||||
"end": 1200,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "11 / SAUNA BENCHES & STONE HEATER",
|
||||
"start": 1201,
|
||||
"end": 1320,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "12 / OUTDOOR SHOWER",
|
||||
"start": 1321,
|
||||
"end": 1440,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "13 / TWO RAINWATER TANKS",
|
||||
"start": 1441,
|
||||
"end": 1560,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "14 / ROOFTOP SOLAR & STARLINK",
|
||||
"start": 1561,
|
||||
"end": 1680,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
|
||||
"start": 1681,
|
||||
"end": 1800,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "16 / FOOD GARDEN & GREENHOUSE",
|
||||
"start": 1801,
|
||||
"end": 1920,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "17 / INSIDE THE GREENHOUSE",
|
||||
"start": 1921,
|
||||
"end": 2040,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "18 / QUIET MEDITATION ROOM",
|
||||
"start": 2041,
|
||||
"end": 2160,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "19 / CUSHION, CANDLES & STILLNESS",
|
||||
"start": 2161,
|
||||
"end": 2280,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "20 / GRAVEL ROAD & GARAGE",
|
||||
"start": 2281,
|
||||
"end": 2400,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
|
||||
"start": 2401,
|
||||
"end": 2520,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "22 / FUTURE SPACES - BUILD LATER",
|
||||
"start": 2521,
|
||||
"end": 2640,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "23 / A HOME THAT CAN CHANGE",
|
||||
"start": 2641,
|
||||
"end": 2760,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "24 / BOOKS & A QUIET READING CORNER",
|
||||
"start": 2761,
|
||||
"end": 2880,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "25 / TRAINING & STRENGTH",
|
||||
"start": 2881,
|
||||
"end": 3000,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "26 / OUTDOOR TABLE & MATCHA",
|
||||
"start": 3001,
|
||||
"end": 3120,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "27 / PLAY & FUTURE BASKETBALL",
|
||||
"start": 3121,
|
||||
"end": 3240,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "28 / WALKING & FOOD GARDEN",
|
||||
"start": 3241,
|
||||
"end": 3360,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "29 / EASY EVERYDAY FOOD",
|
||||
"start": 3361,
|
||||
"end": 3480,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "30 / COPPER & MARBLE KITCHEN",
|
||||
"start": 3481,
|
||||
"end": 3600,
|
||||
"seconds": 5
|
||||
}
|
||||
]
|
||||
182
source/samford-retreat-garage/tour-chapters-garden-flow.json
Normal file
182
source/samford-retreat-garage/tour-chapters-garden-flow.json
Normal file
@@ -0,0 +1,182 @@
|
||||
[
|
||||
{
|
||||
"title": "01 / ONE CONNECTED GARDEN HOMESTEAD",
|
||||
"start": 1,
|
||||
"end": 120,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "02 / LIVING PAVILION & VERANDA",
|
||||
"start": 121,
|
||||
"end": 240,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
|
||||
"start": 241,
|
||||
"end": 360,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "04 / DINING & OPEN LIVING",
|
||||
"start": 361,
|
||||
"end": 480,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "05 / ONE SERVER - LOCAL AI",
|
||||
"start": 481,
|
||||
"end": 600,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
|
||||
"start": 601,
|
||||
"end": 720,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "07 / ADJACENT BEDROOM WING",
|
||||
"start": 721,
|
||||
"end": 840,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "08 / ENCLOSED BEDROOM",
|
||||
"start": 841,
|
||||
"end": 960,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
|
||||
"start": 961,
|
||||
"end": 1080,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
|
||||
"start": 1081,
|
||||
"end": 1200,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "11 / SAUNA BENCHES & STONE HEATER",
|
||||
"start": 1201,
|
||||
"end": 1320,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "12 / OUTDOOR SHOWER",
|
||||
"start": 1321,
|
||||
"end": 1440,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "13 / TWO RAINWATER TANKS",
|
||||
"start": 1441,
|
||||
"end": 1560,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "14 / ROOFTOP SOLAR & STARLINK",
|
||||
"start": 1561,
|
||||
"end": 1680,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
|
||||
"start": 1681,
|
||||
"end": 1800,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "16 / FOOD GARDEN & GREENHOUSE",
|
||||
"start": 1801,
|
||||
"end": 1920,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "17 / INSIDE THE GREENHOUSE",
|
||||
"start": 1921,
|
||||
"end": 2040,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "18 / QUIET MEDITATION ROOM",
|
||||
"start": 2041,
|
||||
"end": 2160,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "19 / CUSHION, CANDLES & STILLNESS",
|
||||
"start": 2161,
|
||||
"end": 2280,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "20 / GRAVEL ROAD & GARAGE",
|
||||
"start": 2281,
|
||||
"end": 2400,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
|
||||
"start": 2401,
|
||||
"end": 2520,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "22 / BALANCED FUTURE PAVILIONS",
|
||||
"start": 2521,
|
||||
"end": 2640,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "23 / RECTANGLES AND GARDEN CURVES",
|
||||
"start": 2641,
|
||||
"end": 2760,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "24 / BOOKS & A QUIET READING CORNER",
|
||||
"start": 2761,
|
||||
"end": 2880,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "25 / TRAINING & STRENGTH",
|
||||
"start": 2881,
|
||||
"end": 3000,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "26 / OUTDOOR TABLE & MATCHA",
|
||||
"start": 3001,
|
||||
"end": 3120,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "27 / FLOWERING GARDEN - ROOM TO BREATHE",
|
||||
"start": 3121,
|
||||
"end": 3240,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "28 / FLOWING WALKS AND GARDEN COURT",
|
||||
"start": 3241,
|
||||
"end": 3360,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "29 / EASY EVERYDAY FOOD",
|
||||
"start": 3361,
|
||||
"end": 3480,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "30 / COPPER & MARBLE KITCHEN",
|
||||
"start": 3481,
|
||||
"end": 3600,
|
||||
"seconds": 5
|
||||
}
|
||||
]
|
||||
146
source/samford-retreat-garage/tour-chapters-reading.json
Normal file
146
source/samford-retreat-garage/tour-chapters-reading.json
Normal file
@@ -0,0 +1,146 @@
|
||||
[
|
||||
{
|
||||
"title": "01 / THE SPACIOUS HOMESTEAD",
|
||||
"start": 1,
|
||||
"end": 120,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "02 / LIVING PAVILION & VERANDA",
|
||||
"start": 121,
|
||||
"end": 240,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
|
||||
"start": 241,
|
||||
"end": 360,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "04 / DINING & OPEN LIVING",
|
||||
"start": 361,
|
||||
"end": 480,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "05 / ONE SERVER - LOCAL AI",
|
||||
"start": 481,
|
||||
"end": 600,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
|
||||
"start": 601,
|
||||
"end": 720,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "07 / ADJACENT BEDROOM WING",
|
||||
"start": 721,
|
||||
"end": 840,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "08 / ENCLOSED BEDROOM",
|
||||
"start": 841,
|
||||
"end": 960,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
|
||||
"start": 961,
|
||||
"end": 1080,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
|
||||
"start": 1081,
|
||||
"end": 1200,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "11 / SAUNA BENCHES & STONE HEATER",
|
||||
"start": 1201,
|
||||
"end": 1320,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "12 / OUTDOOR SHOWER",
|
||||
"start": 1321,
|
||||
"end": 1440,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "13 / TWO RAINWATER TANKS",
|
||||
"start": 1441,
|
||||
"end": 1560,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "14 / ROOFTOP SOLAR & STARLINK",
|
||||
"start": 1561,
|
||||
"end": 1680,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
|
||||
"start": 1681,
|
||||
"end": 1800,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "16 / FOOD GARDEN & GREENHOUSE",
|
||||
"start": 1801,
|
||||
"end": 1920,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "17 / INSIDE THE GREENHOUSE",
|
||||
"start": 1921,
|
||||
"end": 2040,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "18 / QUIET MEDITATION ROOM",
|
||||
"start": 2041,
|
||||
"end": 2160,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "19 / CUSHION, CANDLES & STILLNESS",
|
||||
"start": 2161,
|
||||
"end": 2280,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "20 / GRAVEL ROAD & GARAGE",
|
||||
"start": 2281,
|
||||
"end": 2400,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "21 / TESLA, SOLAR CHARGING & WORKSHOP",
|
||||
"start": 2401,
|
||||
"end": 2520,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "22 / FUTURE SPACES - BUILD LATER",
|
||||
"start": 2521,
|
||||
"end": 2640,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "23 / A HOME THAT CAN CHANGE",
|
||||
"start": 2641,
|
||||
"end": 2760,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "24 / BOOKS & A QUIET READING CORNER",
|
||||
"start": 2761,
|
||||
"end": 2880,
|
||||
"seconds": 5
|
||||
}
|
||||
]
|
||||
140
source/samford-retreat-garage/tour-chapters.json
Normal file
140
source/samford-retreat-garage/tour-chapters.json
Normal file
@@ -0,0 +1,140 @@
|
||||
[
|
||||
{
|
||||
"title": "01 / THE SPACIOUS HOMESTEAD",
|
||||
"start": 1,
|
||||
"end": 120,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "02 / LIVING PAVILION & VERANDA",
|
||||
"start": 121,
|
||||
"end": 240,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
|
||||
"start": 241,
|
||||
"end": 360,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "04 / DINING & OPEN LIVING",
|
||||
"start": 361,
|
||||
"end": 480,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "05 / ONE SERVER - LOCAL AI",
|
||||
"start": 481,
|
||||
"end": 600,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
|
||||
"start": 601,
|
||||
"end": 720,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "07 / ADJACENT BEDROOM WING",
|
||||
"start": 721,
|
||||
"end": 840,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "08 / ENCLOSED BEDROOM",
|
||||
"start": 841,
|
||||
"end": 960,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
|
||||
"start": 961,
|
||||
"end": 1080,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
|
||||
"start": 1081,
|
||||
"end": 1200,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "11 / SAUNA BENCHES & STONE HEATER",
|
||||
"start": 1201,
|
||||
"end": 1320,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "12 / OUTDOOR SHOWER",
|
||||
"start": 1321,
|
||||
"end": 1440,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "13 / TWO RAINWATER TANKS",
|
||||
"start": 1441,
|
||||
"end": 1560,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "14 / ROOFTOP SOLAR & STARLINK",
|
||||
"start": 1561,
|
||||
"end": 1680,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
|
||||
"start": 1681,
|
||||
"end": 1800,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "16 / FOOD GARDEN & GREENHOUSE",
|
||||
"start": 1801,
|
||||
"end": 1920,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "17 / INSIDE THE GREENHOUSE",
|
||||
"start": 1921,
|
||||
"end": 2040,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "18 / QUIET MEDITATION ROOM",
|
||||
"start": 2041,
|
||||
"end": 2160,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "19 / CUSHION, CANDLES & STILLNESS",
|
||||
"start": 2161,
|
||||
"end": 2280,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "20 / GRAVEL ROAD & GARAGE",
|
||||
"start": 2281,
|
||||
"end": 2400,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "21 / CAR, WORKBENCH & BIKE RACKS",
|
||||
"start": 2401,
|
||||
"end": 2520,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "22 / FUTURE SPACES - BUILD LATER",
|
||||
"start": 2521,
|
||||
"end": 2640,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "23 / A HOME THAT CAN CHANGE",
|
||||
"start": 2641,
|
||||
"end": 2760,
|
||||
"seconds": 5
|
||||
}
|
||||
]
|
||||
19
source/samford-retreat-garage/verify_daily_life.py
Normal file
19
source/samford-retreat-garage/verify_daily_life.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import bpy,json,os
|
||||
s=bpy.context.scene;out=os.path.dirname(__file__)
|
||||
chapters=json.load(open(out+'/tour-chapters-daily-life.json'))
|
||||
fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
|
||||
issues=[];captions=[]
|
||||
for c in chapters:
|
||||
frame=c.get('start',c.get('start_frame',1))+30;s.frame_set(frame);bpy.context.view_layer.update()
|
||||
shown=[o.data.body for o in fonts if o.scale.length>.5]
|
||||
captions.append({'frame':frame,'titles':shown})
|
||||
if len(shown)!=1:issues.append({'frame':frame,'titles':shown})
|
||||
required=['OVEN - insulated glass door','INDUCTION - four zone glass cooktop','ISLAND - honed marble countertop','COPPER STOCKPOT','COPPER SAUCEPAN','COPPER FRYING PAN','PULL UP BAR - dedicated steel frame','OUTDOOR TABLE - ten place timber top','SHADED WALKING LOOP - garden and quiet room','HARVEST BENCH - garden sorting surface','PANTRY - tall timber cabinet','FREEZER - compact upright']
|
||||
missing=[n for n in required if n not in bpy.data.objects]
|
||||
report={'chapters':len(chapters),'end_frame':s.frame_end,'fps':s.render.fps,'missing':missing,'caption_issues':issues,'caption_samples':captions,'render_engine':s.render.engine,'render_size':[s.render.resolution_x,s.render.resolution_y],'detail_render_enabled':not bpy.data.collections['DETAIL - full geometry for renders'].hide_render,'fast_excluded_from_render':bpy.data.collections['00 FAST TOUR - lightweight viewport'].hide_render,'training_steps':sum(o.name.startswith('Training access step') for o in s.objects),'play_steps':sum(o.name.startswith('Play deck access step') for o in s.objects)}
|
||||
json.dump(report,open(out+'/daily-life-verification.json','w'),indent=2)
|
||||
assert not missing and not issues,report
|
||||
assert report['chapters']==30 and s.frame_end==3600 and s.render.fps==24
|
||||
assert report['detail_render_enabled'] and report['fast_excluded_from_render']
|
||||
assert report['training_steps']==3 and report['play_steps']==3
|
||||
print('VERIFIED '+json.dumps({k:v for k,v in report.items() if k!='caption_samples'}),flush=True)
|
||||
30
source/samford-retreat-garage/verify_garden_flow.py
Normal file
30
source/samford-retreat-garage/verify_garden_flow.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import bpy,json,os
|
||||
from mathutils import Vector,Matrix
|
||||
O=os.path.dirname(__file__);s=bpy.context.scene
|
||||
chapters=json.load(open(O+'/tour-chapters-garden-flow.json'));fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT'];issues=[]
|
||||
for c in chapters:
|
||||
s.frame_set(c['start']+30);shown=[o.data.body for o in fonts if o.scale.length>.5]
|
||||
if shown!=[c['title']]:issues.append([c['title'],shown])
|
||||
def ctr(n):
|
||||
o=bpy.data.objects[n];return Matrix.LocRotScale(o.location,o.rotation_euler.to_quaternion(),o.scale)@(sum((Vector(v) for v in o.bound_box),Vector())/8)
|
||||
names=[o.name for o in s.objects];garage=ctr('GARAGE - 8m x 7m concrete floor');med=ctr('Meditation | timber insulated floor')
|
||||
report={'no_table_tennis':not any('table tennis' in n.lower() or n.startswith('PLAY DECK') for n in names),'garage_center':[round(garage.x,2),round(garage.y,2)],'meditation_center':[round(med.x,2),round(med.y,2)],'caption_issues':issues,'tour_chapters':len(chapters),'rainwater_tanks':sum(n.startswith('2000 mm') for n in names),'kitchen_preserved':all(n in names for n in ['ISLAND - honed marble countertop','OVEN - insulated glass door','INDUCTION - four zone glass cooktop']),'flower_beds':sum(n.endswith(' mulched soil') for n in names),'render_size':[s.render.resolution_x,s.render.resolution_y,s.render.resolution_percentage],'fast_excluded_from_render':bpy.data.collections['00 FAST TOUR - lightweight viewport'].hide_render}
|
||||
assert report['no_table_tennis'] and not issues and report['rainwater_tanks']==2 and report['kitchen_preserved'] and report['flower_beds']==13
|
||||
assert report['garage_center']==[0,-19] and report['meditation_center']==[-11.8,12]
|
||||
assert s.frame_end==3600 and report['fast_excluded_from_render']
|
||||
layout=json.load(open(O+'/garden-flow-layout.json'))
|
||||
# Sample the shared paths against the occupied meditation and reserved east pavilion.
|
||||
collisions=[]
|
||||
for path in layout['paths']:
|
||||
if path['kind']!='gravel' or 'future threshold' in path['name']:continue
|
||||
for x,y in path['points']:
|
||||
for name,cx in [('meditation',-11.8),('future east',11.8)]:
|
||||
if abs(x-cx)<2.25 and abs(y-12)<2.4:collisions.append([path['name'],name])
|
||||
assert not collisions,collisions
|
||||
report['pavilion_path_intersections']=collisions
|
||||
for name in ['Eucalyptus pale trunk.006','Eucalyptus pale trunk.007']:
|
||||
p=ctr(name);assert not (5.75<p.x<13.05 and -21.6<p.y<-16.4),name
|
||||
report['solar_tree_clearance']=True
|
||||
s['Daily life additions']='Covered exercise area, outdoor table for ten and tea counter, garden court and flowering beds, curved walking routes and quiet bench, breakfast counter, pantry/freezer and harvest bench. Table-tennis deck removed in version 004.'
|
||||
s.frame_set(1);bpy.ops.wm.save_as_mainfile(filepath=O+'/samford-solarpunk-garden-flow-tour.blend')
|
||||
json.dump(report,open(O+'/garden-flow-verification.json','w'),indent=2);print('VERIFIED '+json.dumps(report),flush=True)
|
||||
39
source/samford-spacious-tour/README.md
Normal file
39
source/samford-spacious-tour/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Samford family homestead — press Play
|
||||
|
||||
Open **samford-spacious-guided-tour.blend**. It is saved at frame 1 in the animated camera view.
|
||||
|
||||
Place the pointer over the 3D view and press **Space**, or click the Timeline’s Play triangle. Space pauses. **Shift + Left Arrow** returns to the beginning. If you leave camera view, use **View → Cameras → Active Camera** before playing.
|
||||
|
||||
The tour lasts **110 seconds**, with **22 chapters**. Each chapter uses a short camera move, with deliberate cuts between rooms. It includes the main living pavilion, kitchen, dining, homelab, short corridor, parents’ bedroom, bathroom, sauna exterior and interior, outdoor shower, tanks, Starlink, solar array, garden, greenhouse, each child’s future room, courtyard and overall site. Chapter labels and Timeline markers are included.
|
||||
|
||||
## Final family layout
|
||||
|
||||
- Kitchen, dining and lounge form the central shared room.
|
||||
- The parents’ enclosed bedroom is adjacent, reached by a short corridor approximately 3.5m long and 1.2m wide.
|
||||
- The bathroom has its own door from the shared lobby. Visitors do not pass through the parents’ bedroom.
|
||||
- Three future private children’s rooms replace the earlier generic office, guest, workshop and dwelling reservations. Each has a 4.5 × 4.8m footprint, its own front door, bed, desk, wardrobe and personal display area. Colour accents are replaceable examples, with no prescribed theme for any child.
|
||||
- Covered timber walks connect the three rooms around a courtyard to the shared home. They have no separate kitchens or bathrooms.
|
||||
- The sauna, shower, two tanks, greenhouse and six garden beds remain spread out. The 30 solar panels and one local-AI server are retained.
|
||||
|
||||
These future children’s rooms are fully modelled so their interiors can be toured. Collections 23, 24 and 25 contain them individually; collection 26 contains their shared walks. They are future design proposals, not a claim that they are already built or approved. Their roof-junction finishing strips are in collection 20.
|
||||
|
||||
## Files and display
|
||||
|
||||
- `family-homestead-overview.png`: rendered overview.
|
||||
- `three-childrens-rooms.png`: courtyard and children’s rooms.
|
||||
- `child-room-interior.png`: example adaptable interior.
|
||||
- `family-courtyard-plan.svg`: labelled layout diagram.
|
||||
- `tour-chapters.json`: chapter names and frame ranges.
|
||||
- `tour-checks.json`: verified scene counts, timing, moving cameras and clear corridor doorway.
|
||||
|
||||
The interactive tour uses solid material colours for responsiveness. Glass appears outlined in that viewport mode; the Cycles renderer retains actual glazing and procedural materials. Dense ground grass is hidden in the viewport only. The original earlier models are preserved in their own output folders.
|
||||
|
||||
## Land search guidance
|
||||
|
||||
For two adults and three children wanting this layout, vegetables, fruit trees and general outdoor space, **1–2 hectares of usable land is a reasonable starting search range; about 2 hectares (4.94 acres) is my comfortable planning target**. This is a design estimate, not a statutory minimum or a promise of food self-sufficiency. More land may suit grazing animals or substantial privacy buffers, but pasture quality, water, labour and management become decisive.
|
||||
|
||||
A relatively level, usable hectare can offer more practical room than a larger constrained property. Check the actual address for slope, easements, access, wastewater capacity and hazards. [YourHome site selection](https://www.yourhome.gov.au/buy-build-renovate/choosing-site) explains the role of site constraints. [City of Moreton Bay flood mapping](https://www.moretonbay.qld.gov.au/Services/Property-Ownership/Flooding/Flood-Mapping) distinguishes current flood information from planning overlay maps. [Queensland’s small-property livestock guide](https://era.daf.qld.gov.au/id/eprint/8160/1/a-guide-to-managing-livestock-on-small-properties.pdf) explains why livestock capacity is site-dependent.
|
||||
|
||||
The detached children’s sleeping rooms need project-specific building and planning classification. Do not assume that describing them as bedrooms makes approval automatic, or that a large lot permits multiple independent dwellings. [Council’s secondary-dwelling guidance](https://www.moretonbay.qld.gov.au/Services/Building-Development/Planning-Schemes/MBRC/Info-Sheets/Secondary-Dwelling) illustrates that dwelling type, zoning and siting all matter; it is not a determination of how this particular bedroom-cabin proposal would be classified.
|
||||
|
||||
The central shared room, short private circulation and separation of bedrooms from noisy uses draw on [YourHome orientation guidance](https://www.yourhome.gov.au/passive-design/orientation) and [noise-control guidance](https://www.yourhome.gov.au/live-adapt/noise-control). This remains an architectural concept without a surveyed site, detailed structural design, certified access, fire, moisture, water or off-grid energy assessment.
|
||||
212
source/samford-spacious-tour/build_tour.py
Normal file
212
source/samford-spacious-tour/build_tour.py
Normal file
@@ -0,0 +1,212 @@
|
||||
import bpy, math, os, json, random, bmesh
|
||||
from mathutils import Vector
|
||||
from math import sin,cos,pi
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
|
||||
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
|
||||
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
|
||||
def move(o,delta): o.location+=Vector(delta)
|
||||
def shift_collection(name,d):
|
||||
for o in bpy.data.collections[name].objects: move(o,d)
|
||||
sleep=(-5,-7,0); sauna=(-5,-2,0); tanks=(-5,-4,0); garden=(6,1,0)
|
||||
shift_collection('02 Sleeping pavilion 8 x 5 m',sleep)
|
||||
shift_collection('16 Oval Finnish sauna',sauna)
|
||||
shift_collection('17 Outdoor shower and stone terrace',sauna)
|
||||
shift_collection('06 Kitchen garden',garden)
|
||||
for o in bpy.data.collections['05 Water and services'].objects:
|
||||
p=center(o); n=o.name
|
||||
if n.startswith(('2000 mm','Poly tank','Tank service lid','Accessible tank outlet','Gravel tank pad')): move(o,tanks)
|
||||
elif n.startswith('Exposed rainwater downpipe') and p.x<-10: move(o,sleep)
|
||||
elif p.y<-8 and ('PV ' in n or 'Photovoltaic' in n or 'Ground array' in n): move(o,(3,-6,0))
|
||||
for o in bpy.data.collections['18 Starlink and single-server homelab'].objects:
|
||||
if o.name.startswith('Starlink'): move(o,sleep)
|
||||
if o.name=='Overview sauna label': o.hide_render=True; o.hide_set(True)
|
||||
for o in bpy.data.collections['19 Clay finishes and hemp wall infill'].objects:
|
||||
if o.name.startswith('Sleeping'): move(o,sleep)
|
||||
for o in list(bpy.data.collections['12 Natural materials and reversible connections'].objects):
|
||||
p=center(o)
|
||||
if o.name.startswith('Sleeping west expansion'): move(o,sleep)
|
||||
elif o.name.startswith('Stone facing bed joint'):
|
||||
if p.x<-12 and p.y<-5: move(o,sleep)
|
||||
elif p.x<-6: bpy.data.objects.remove(o,do_unlink=True)
|
||||
# Guest and office are optional attached rooms; short connecting frames will be rebuilt.
|
||||
for o in list(bpy.data.collections['13 NEXT - optional attached room modules'].objects):
|
||||
if o.name.startswith('02A'): move(o,(5,0,0))
|
||||
elif o.name.startswith('02B'): move(o,(-7,-7,0))
|
||||
else: bpy.data.objects.remove(o,do_unlink=True)
|
||||
for o in list(bpy.data.collections['14 LATER - independent dwelling and workshop'].objects):
|
||||
if o.name.startswith('03A'): move(o,(0,9,0))
|
||||
elif o.name.startswith('03B'): move(o,(0,-8,0))
|
||||
else: bpy.data.objects.remove(o,do_unlink=True)
|
||||
for o in bpy.data.collections['15 Expansion labels - toggle to hide'].objects: o.hide_set(True); o.hide_render=True
|
||||
# Move the tree whose former location is inside the newly separated guest reservation.
|
||||
tree_delta=(0,0,0)
|
||||
for o in bpy.data.collections['09 Eucalyptus and fruit trees'].objects:
|
||||
if 'trunk' in o.name:
|
||||
p=center(o); tree_delta=(-7,0,0) if -35<p.x<-33 and -17<p.y<-13 else (0,0,0)
|
||||
if 'Fruit tree' in o.name: tree_delta=garden
|
||||
if tree_delta!=(0,0,0): move(o,tree_delta)
|
||||
# Remove old hard-edged routes and rebuild a connected, terrain-following gravel network.
|
||||
for o in list(bpy.data.collections['07 Terrain and paths'].objects):
|
||||
if 'path' in o.name.lower() or 'gravel' in o.name.lower(): bpy.data.objects.remove(o,do_unlink=True)
|
||||
for o in list(bpy.data.collections['17 Outdoor shower and stone terrace'].objects):
|
||||
if o.name.startswith('Loose gravel sauna access'): bpy.data.objects.remove(o,do_unlink=True)
|
||||
for o in list(bpy.data.collections['03 Covered breezeway'].objects): bpy.data.objects.remove(o,do_unlink=True)
|
||||
COL={}; active='new'
|
||||
for key,name in [('new','20 Spacious site connections'),('bath','21 Bathroom fittings'),('tour','22 PLAY - guided camera tour')]:
|
||||
c=bpy.data.collections.new(name); s.collection.children.link(c); COL[key]=c
|
||||
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']; roof=bpy.data.materials['Pale Colorbond - powder coated steel']; gravel=bpy.data.materials['Loose local gravel - fine stones']; clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; glass=bpy.data.materials['Clear glazing']; F=1.12
|
||||
for cx,cy,w,d in [(-11.1,-1.8,10.2,1.6),(-16.2,-7.45,1.6,11.3)]:
|
||||
box('Extended covered walkway bearer',(cx,cy,F-.18),(w,d,.18),wood)
|
||||
for i in range(int(w/.14)):
|
||||
box('Extended walkway timber deck board',(cx-w/2+.07+i*.14,cy,F-.035),(.13,d,.06),wood,.003)
|
||||
box('Extended walkway pale roof',(cx,cy,F+2.7),(w+.20,d+.20,.065),roof,.008)
|
||||
count=max(2,int(max(w,d)/2.4)+1)
|
||||
for j in range(count):
|
||||
a=j/(count-1)
|
||||
for side in [-1,1]:
|
||||
x=cx-w/2+.12+a*(w-.24) if w>d else cx+side*(w/2-.12)
|
||||
y=cy+side*(d/2-.12) if w>d else cy-d/2+.12+a*(d-.24)
|
||||
g=ground(x,y); box('Walkway stone pier',(x,y,(g+F-.25)/2),(.27,.27,F-.25-g),stone,.01)
|
||||
box('Walkway exposed timber post',(x,y,F+1.32),(.10,.10,2.64),woodz,.005)
|
||||
if w>d:
|
||||
for j in range(int(w/.6)+1): box('Walkway exposed rafter',(cx-w/2+j*.6,cy,F+2.64),(.055,d,.10),wood)
|
||||
else:
|
||||
for j in range(int(d/.6)+1): box('Walkway exposed rafter',(cx,cy-d/2+j*.6,F+2.64),(w,.055,.10),wood)
|
||||
nextmat=bpy.data.materials['NEXT - warm timber ghost frame']
|
||||
for x1,x2,y in [(6.25,12.8,.8),(-27,-23.25,-15.2)]:
|
||||
for yy in [y-.8,y+.8]:
|
||||
beam('Optional extension link floor',(x1,yy,F),(x2,yy,F),.07,.10,nextmat)
|
||||
beam('Optional extension link roof',(x1,yy,F+2.75),(x2,yy,F+2.75),.07,.10,nextmat)
|
||||
def path(name,points,width):
|
||||
vs=[]; fs=[]
|
||||
for a,b in zip(points,points[1:]):
|
||||
av,bv=Vector(a),Vector(b); dv=bv-av; side=Vector((-dv.y,dv.x)).normalized()*width/2
|
||||
n=max(2,int(dv.length/.4))
|
||||
for i in range(n):
|
||||
p=av+dv*i/(n-1)
|
||||
for sidep in [p-side,p+side]: vs.append((sidep.x,sidep.y,ground(sidep.x,sidep.y)+.045))
|
||||
k=len(vs)-n*2
|
||||
fs.extend([(k+i*2,k+i*2+1,k+i*2+3,k+i*2+2) for i in range(n-1)])
|
||||
mesh(name,vs,fs,gravel)
|
||||
path('Main meandering gravel arrival',[(1,17),(0,10),(0,7)],1.6)
|
||||
path('Sauna and tanks gravel walk',[(0,7),(-8,6),(-15,6),(-22,6.6),(-25,5.9),(-27,2),(-27,-5),(-24,-8)],1.2)
|
||||
path('Garden and greenhouse gravel walk',[(0,7),(9,9),(19,9),(26,6),(26,-4),(24,-5)],1.25)
|
||||
path('Sleeping pavilion garden walk',[(-8,6),(-12,2),(-12,-11),(-15.5,-12.5)],1.1)
|
||||
path('Future north dwelling reserved access',[(-8,6),(-7,15),(-9,21)],.8)
|
||||
path('Future workshop reserved access',[(0,-3.5),(0,-18),(0,-25)],.8)
|
||||
# Stretch ground-contact supports to the actual ground after relocation.
|
||||
for o in s.objects:
|
||||
if o.type!='MESH': continue
|
||||
if any(t in o.name.lower() for t in ['pier','foundation support','tank pad']):
|
||||
p=center(o); zmin=min((o.matrix_world@v.co).z for v in o.data.vertices); g=ground(p.x,p.y)
|
||||
if g<max((o.matrix_world@v.co).z for v in o.data.vertices)-.05:
|
||||
inv=o.matrix_world.inverted()
|
||||
for v in o.data.vertices:
|
||||
q=o.matrix_world@v.co
|
||||
if abs(q.z-zmin)<.001: q.z=g; v.co=inv@q
|
||||
# Complete the bathroom alcove so the interior tour can identify its actual purpose.
|
||||
active='bath'; ceramic=mat('Bathroom warm white ceramic',(.76,.73,.65),.23); water=mat('Bathroom satin stainless',(.34,.38,.38),.32,.75)
|
||||
box('Bathroom mineral floor',(-15.94,-15.92,F+.02),(1.50,3.85,.045),stone,.008)
|
||||
box('Bathroom toilet cistern',(-15.83,-17.74,F+.65),(.44,.19,.65),ceramic,.08)
|
||||
box('Bathroom toilet pedestal',(-15.83,-17.38,F+.22),(.29,.40,.43),ceramic,.10)
|
||||
box('Bathroom toilet bowl',(-15.83,-17.29,F+.43),(.43,.59,.15),ceramic,.15)
|
||||
box('Bathroom toilet seat',(-15.83,-17.25,F+.52),(.40,.55,.055),ceramic,.12)
|
||||
box('Bathroom floating timber vanity',(-15.50,-15.7,F+.64),(.45,.82,.28),wood,.02)
|
||||
box('Bathroom basin',(-15.50,-15.7,F+.85),(.42,.65,.11),ceramic,.055)
|
||||
rod('Bathroom tap',(-15.30,-15.8,F+.9),(-15.30,-15.8,F+1.13),.015,water)
|
||||
box('Bathroom mirror',(-15.12,-15.7,F+1.51),(.022,.72,.8),water,.025)
|
||||
box('Bathroom shower tray',(-15.93,-14.0,F+.04),(1.45,1.18,.08),stone,.012)
|
||||
rod('Bathroom shower riser',(-15.15,-13.97,F+.35),(-15.15,-13.97,F+2.15),.017,water)
|
||||
rod('Bathroom shower arm',(-15.15,-13.97,F+2.15),(-15.58,-13.97,F+2.15),.017,water)
|
||||
rod('Bathroom shower head',(-15.58,-13.97,F+2.10),(-15.58,-13.97,F+2.14),.10,water,sides=24)
|
||||
# Gentle interior fill lights are visible fixtures, rather than relying on outdoor sunlight.
|
||||
for name,p,power in [('Living ceiling light',(-2,-.5,3.7),220),('Sleeping ceiling light',(-19,-15,3.7),130),('Bathroom ceiling light',(-15.9,-15.4,3.55),65)]:
|
||||
d=bpy.data.lights.new(name,'AREA'); d.energy=power; d.size=2; d.color=(1,.85,.68); ob=bpy.data.objects.new(name,d); COL[active].objects.link(ob); ob.location=p
|
||||
# One animated camera with explicit shot-boundary cuts is dependable in Timeline playback.
|
||||
active='tour'; s.timeline_markers.clear()
|
||||
camdata=bpy.data.cameras.new('PLAY TOUR camera'); cam=bpy.data.objects.new('PLAY TOUR camera',camdata); COL[active].objects.link(cam); camdata.clip_start=.04; camdata.clip_end=450; camdata.lens=34; s.camera=cam
|
||||
shots=[
|
||||
('01 / THE SPACIOUS HOMESTEAD',(51,71,48),(45,65,43),(-4,-3,1),(-5,-4,1),44),
|
||||
('02 / LIVING PAVILION & VERANDA',(12,17,6),(8,12,5),(0,3,2),(0,2.8,2),36),
|
||||
('03 / KITCHEN & EXPOSED STRUCTURE',(-.2,1.8,2.9),(-1.2,1.25,2.75),(-3,-2.35,2.0),(-3.2,-2.35,2.1),30),
|
||||
('04 / DINING & OPEN LIVING',(-4.8,1.8,2.9),(-4.1,2,2.7),(.2,.1,1.9),(.9,-.1,2),30),
|
||||
('05 / ONE SERVER - LOCAL AI',(1,-.85,3.55),(1.25,-1,3.3),(3.9,-2.3,1.95),(4.2,-2.3,1.9),36),
|
||||
('06 / LONG COVERED WALK',(-7.2,-.8,2.75),(-9.5,-.8,2.75),(-16.2,-2.4,2),(-16.2,-4,2),30),
|
||||
('07 / BEDROOM + BATHROOM PAVILION',(-8,-5,7),(-10,-7,6),(-19,-15.5,2),(-19,-15.5,2),35),
|
||||
('08 / THE BEDROOM',(-17.6,-13.8,2.8),(-17.9,-14,2.7),(-20,-15.8,1.7),(-20,-16,1.7),26),
|
||||
('09 / THE BATHROOM',(-16.25,-14.9,2.95),(-16.15,-15.1,2.9),(-15.7,-17,1.7),(-15.7,-17.1,1.7),22),
|
||||
('10 / FINNISH SAUNA & OUTDOOR SHOWER',(-14,15,6.5),(-16,12,5.5),(-24,2.7,1.7),(-24,2.6,1.6),37),
|
||||
('11 / SAUNA BENCHES & STONE HEATER',(-23.0,3.62,2.18),(-23.25,3.62,2.18),(-23,1.2,1.95),(-23.2,1.2,1.95),20),
|
||||
('12 / OUTDOOR SHOWER',(-24.5,7,2.9),(-25,6.5,2.7),(-27,4.4,1.4),(-27,4.4,1.4),34),
|
||||
('13 / TWO RAINWATER TANKS',(-18,-1,4.4),(-19,-2.5,4.1),(-25,-8,1),(-25,-8,1),40),
|
||||
('14 / ROOFTOP SOLAR & STARLINK',(-4,-3,10),(-7,-5,9),(-15.5,-13.1,4.6),(-15.5,-13.1,4.6),36),
|
||||
('15 / SOLAR ARRAY & ENERGY STORAGE',(22,-7,7),(20,-9,6),(13,-17.5,1.5),(13,-17.5,1.5),36),
|
||||
('16 / FOOD GARDEN & GREENHOUSE',(34,10,9),(32,7,7),(25,-3,1),(25,-4,1),34),
|
||||
('17 / INSIDE THE GREENHOUSE',(24.2,-5.7,2.6),(24.2,-6.3,2.5),(24.2,-9.0,1.3),(24.2,-9.5,1.3),24),
|
||||
('18 / ADD ROOMS WHEN NEEDED',(26,12,11),(22,10,10),(15.8,1,2),(15.8,1,2),37),
|
||||
('19 / FUTURE DWELLING & WORKSHOP',(48,69,51),(44,64,47),(-6,-2,1),(-6,-2,1),40),
|
||||
('20 / LIVE FIRST. ADD LATER.',(44,64,47),(51,71,51),(-6,-2,1),(-6,-2,1),40),
|
||||
]
|
||||
FPS=24; N=120; s.render.fps=FPS; s.frame_start=1; s.frame_end=N*len(shots); s.sync_mode='FRAME_DROP'
|
||||
caption=mat('Tour captions ivory',(.92,.94,.86),.6); caption.node_tree.nodes['Principled BSDF'].inputs['Emission Color'].default_value=(.92,.94,.86,1); caption.node_tree.nodes['Principled BSDF'].inputs['Emission Strength'].default_value=1
|
||||
schedule=[]
|
||||
for i,(title,a,b,ta,tb,lens) in enumerate(shots):
|
||||
start=i*N+1; end=(i+1)*N
|
||||
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); camdata.lens=lens; camdata.keyframe_insert('lens',frame=frame)
|
||||
marker=s.timeline_markers.new(title,frame=start)
|
||||
cu=bpy.data.curves.new(title,'FONT'); cu.body=title; cu.size=.036; cu.align_x='LEFT'; ob=bpy.data.objects.new(title,cu); COL[active].objects.link(ob); cu.materials.append(caption); ob.parent=cam
|
||||
half=36/lens; ob.location=(-half*.91,half*(9/16)*.84,-2); ob.show_in_front=True
|
||||
for f,value in [(0,0),(start-1,0),(start,1),(end,1),(end+1,0)]: ob.scale=(value,)*3; ob.keyframe_insert('scale',frame=f)
|
||||
schedule.append({'title':title,'start':start,'end':end,'seconds':5})
|
||||
# Linear moves within a shot; constant title visibility changes.
|
||||
def curves(action):
|
||||
if not action: return []
|
||||
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,camdata]:
|
||||
for fc in curves(owner.animation_data.action):
|
||||
for k in fc.keyframe_points: k.interpolation='LINEAR'
|
||||
for o in COL['tour'].objects:
|
||||
if o.type=='FONT':
|
||||
for fc in curves(o.animation_data.action):
|
||||
for k in fc.keyframe_points: k.interpolation='CONSTANT'
|
||||
# Viewport tour uses material colours for responsive playback. Glazing is outlined so interiors remain visible.
|
||||
for o in s.objects:
|
||||
if o.type=='MESH' and any(sl.material==glass for sl in o.material_slots): o.display_type='WIRE'
|
||||
if o.type=='MESH' and o.name.startswith(('Native grasses','Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): o.hide_set(True)
|
||||
for screen in bpy.data.screens:
|
||||
for area in screen.areas:
|
||||
if area.type=='VIEW_3D':
|
||||
sp=area.spaces.active; sp.region_3d.view_perspective='CAMERA'; sp.region_3d.view_camera_zoom=0; sp.shading.type='SOLID'; sp.shading.color_type='MATERIAL'; sp.shading.light='STUDIO'; sp.shading.show_shadows=True; sp.shading.show_cavity=True; sp.overlay.show_overlays=False; sp.clip_end=450
|
||||
if hasattr(sp,'use_local_camera'): sp.use_local_camera=False
|
||||
for area in screen.areas:
|
||||
if area.type=='DOPESHEET_EDITOR': area.spaces.active.show_region_ui=False
|
||||
s.frame_set(1); s.render.engine='CYCLES'; s.render.resolution_x=2560; s.render.resolution_y=1440; s.render.resolution_percentage=100; s.cycles.samples=40; s.cycles.use_denoising=True
|
||||
s['PLAY THE TOUR']='Press Space with the mouse over the 3D viewport, or click Play in Timeline. Shift+Left Arrow returns to the beginning. 100 seconds / 20 scenes at 24 fps. Saved in active camera view.'
|
||||
s['Spacious layout']='Sleeping pavilion moved 5m west and 7m south: 10m north-south clear gap from living footprint. Covered L walkway rebuilt. Sauna moved 5m west/2m south; tanks 5m west/4m south; garden 6m east/1m north. Optional future sites spaced farther apart.'
|
||||
s['Adjoining pavilion explanation']='The second rectangle is the bedroom and bathroom pavilion, not a standalone toilet. Bathroom fixtures now modelled for the tour.'
|
||||
s['Viewport performance']='Dense leaf/grass meshes are hidden only in viewport, retained for final renders. Glass uses outline display in solid viewport. Restore via Alt+H if desired.'
|
||||
txt=bpy.data.texts.new('START HERE - PRESS PLAY'); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Spacious layout']+'\n\n'+s['Adjoining pavilion explanation']+'\n\n'+s['Viewport performance'])
|
||||
json.dump(schedule,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
|
||||
names=[o.name for o in s.objects]
|
||||
report={'duration_seconds':100,'frames':s.frame_end,'shots':len(shots),'tank_count':sum(n.startswith('2000 mm') for n in names),'solar_panels':sum(n.startswith('PV panel glass and cells') for n in names),'garden_beds':sum(n.startswith('Raised vegetable bed soil') for n in names),'servers':sum(n.startswith('ONE LOCAL AI SERVER') for n in names),'sleeping_center':[-19,-15.5],'north_south_clear_gap_metres':10,'active_camera':cam.name,'bathroom_fitted':True}
|
||||
assert report['tank_count']==2 and report['solar_panels']==30 and report['garden_beds']==6 and report['servers']==1
|
||||
json.dump(report,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
|
||||
# Contact sheets can be made from quick workbench renders without waiting for a full movie.
|
||||
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; s.display.shading.background_type='WORLD'; s.world.color=(.19,.23,.22)
|
||||
for i in range(len(shots)):
|
||||
s.frame_set(i*N+60); s.render.filepath=os.path.join(OUT,'check-%02d.png'%(i+1)); bpy.ops.render.render(write_still=True)
|
||||
print('TOUR_BUILT_AND_CHAPTERS_RENDERED',flush=True)
|
||||
151
source/samford-spacious-tour/children_and_final_tour.py
Normal file
151
source/samford-spacious-tour/children_and_final_tour.py
Normal file
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
40
source/samford-spacious-tour/create_family_plan.py
Normal file
40
source/samford-spacious-tour/create_family_plan.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from pathlib import Path
|
||||
from html import escape
|
||||
O=Path(__file__).parent
|
||||
p=['<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1080" viewBox="0 0 1400 1080"><rect width="1400" height="1080" fill="#f1eee5"/><style>text{font-family:Arial,sans-serif;fill:#263a34}.small{font-size:13px}.body{font-size:16px}.title{font-size:32px;font-weight:600}.label{font-size:13px;font-weight:600}</style><text x="60" y="60" class="title">One family. Room to become yourself.</text><text x="60" y="94" class="body">SAMFORD VALLEY / Shared kitchen + dining, adjacent parents’ room, three future children’s rooms</text>']
|
||||
def pt(x,y):return (690+17*x,520-17*y)
|
||||
def label(x,y,lines,size=13):
|
||||
X,Y=pt(x,y)
|
||||
for i,t in enumerate(lines):p.append(f'<text x="{X}" y="{Y+i*(size+3)}" text-anchor="middle" font-size="{size}">{escape(t)}</text>')
|
||||
def rect(cx,cy,w,d,fill,stroke='#4b6256',dash=''):
|
||||
X,Y=pt(cx-w/2,cy+d/2);p.append(f'<rect x="{X}" y="{Y}" width="{w*17}" height="{d*17}" rx="2" fill="{fill}" stroke="{stroke}" stroke-width="2" {dash}/>')
|
||||
def line(points,color='#a99372',width=16):
|
||||
ps=' '.join(f'{pt(x,y)[0]},{pt(x,y)[1]}' for x,y in points);p.append(f'<polyline points="{ps}" fill="none" stroke="{color}" stroke-width="{width}" stroke-linejoin="round"/>')
|
||||
# Covered walks first, with clear building footprints drawn over them.
|
||||
line([(-3,5.4),(-3,14.6)],width=20);line([(-11,9), (7,9),(7,12.6)],width=20)
|
||||
rect(0,0,12,6,'#ddc8a9');label(0,.8,['CENTRAL ROOM','Kitchen · dining · lounge','Local AI desk'],13)
|
||||
rect(0,4.2,12,2.4,'#c8d0b8');label(0,4.3,['NORTH VERANDA'],12)
|
||||
rect(-7.75,-1.8,3.5,1.2,'#c7bfae');label(-7.9,1.0,['Short corridor','3.5 × 1.2 m'],12)
|
||||
rect(-11.8,-4.9,8,5,'#ddc8a9');rect(-12.7,-4.9,6.2,5,'#ddc8a9');label(-12.7,-4.5,['PARENTS’','BEDROOM'],12)
|
||||
rect(-8.7,-5.63,1.8,3.54,'#b6c4c5');label(-8.7,-5.25,['Bath'],10);label(-8.7,-3.0,['Lobby'],9)
|
||||
for i,(x,y,color) in enumerate([(-11,12,'#d8b4a0'),(-3,17,'#b9c9b8'),(7,15,'#aec4d0')]):
|
||||
rect(x,y,4.5,4.8,color,dash='stroke-dasharray="6 3"');label(x,y+.6,[f'CHILD {i+1}','Own room','4.5 × 4.8 m'],11)
|
||||
label(-1,11.6,['SHARED','COURTYARD'],12)
|
||||
X,Y=pt(-23,2);p.append(f'<ellipse cx="{X}" cy="{Y}" rx="38" ry="32" fill="#c5a378" stroke="#4b6256" stroke-width="2"/>');label(-23,2.1,['SAUNA'],12)
|
||||
rect(-27,4.6,1.3,1.3,'#b6c4c5');label(-27,7,['Outdoor','shower'],12)
|
||||
for x in [-23.95,-26.25]:
|
||||
X,Y=pt(x,-8);p.append(f'<circle cx="{X}" cy="{Y}" r="17" fill="#6b7f61"/>')
|
||||
label(-25,-10,['Rainwater tanks'],12)
|
||||
rect(24.2,-7.5,4.3,5,'#c3d6c1');label(24.2,-7,['GREENHOUSE'],11)
|
||||
for x in [23.8,27.5]:
|
||||
for y in [-1.7,1.5,4.7]:rect(x,y,2.3,1.3,'#b29b73')
|
||||
label(25.5,7,['FOOD GARDEN'],13)
|
||||
rect(13.2,-17.7,7.3,5.2,'#71848b');label(13.2,-17,['GROUND SOLAR'],12)
|
||||
label(0,-7.5,['Roof solar on main pavilion','Starlink on parents’ wing'],12)
|
||||
line([(0,7),(9,9),(19,9),(26,6)],color='#d2ccb9',width=7)
|
||||
line([(-3,5.4),(-15,6),(-22,6.6),(-25,5.9)],color='#d2ccb9',width=7)
|
||||
# Diagram furniture indicates that children personalise the same simple base room.
|
||||
for x,y in [(-11,12),(-3,17),(7,15)]:
|
||||
rect(x-1.4,y-.7,.7,1.5,'#f3eadb',stroke='#879387')
|
||||
p.extend(['<path d="M1260 215V148m0 0l-9 18m9-18l9 18" fill="none" stroke="#263a34" stroke-width="3"/><text x="1260" y="135" text-anchor="middle" class="body">N</text>','<line x1="75" y1="890" x2="245" y2="890" stroke="#263a34" stroke-width="4"/><text x="75" y="920" class="small">10 metres</text>','<line x1="60" y1="960" x2="1340" y2="960" stroke="#bdc3b6"/><text x="60" y="992" class="body">Dashed cabins = future rooms for the three children. Shared kitchen, dining and bathroom stay in the family home.</text>','<text x="60" y="1020" class="small">Illustrative layout, not a surveyed block or construction drawing. No property boundary or approval is implied.</text>','<text x="60" y="1044" class="small">Land search guide: 1–2 hectares of usable land; around 2 hectares offers more flexibility. Site conditions decide suitability.</text></svg>'])
|
||||
(O/'family-courtyard-plan.svg').write_text(''.join(p))
|
||||
70
source/samford-spacious-tour/finalize_and_render.py
Normal file
70
source/samford-spacious-tour/finalize_and_render.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import bpy, os, math, bmesh, json
|
||||
from mathutils import Vector
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
|
||||
c=bpy.data.collections['20 Spacious site connections']
|
||||
clay=bpy.data.materials['Soft warm clay plaster - natural mineral texture']; wood=bpy.data.materials['Unpainted hardwood - long X grain']
|
||||
def box(n,p,dims,mat):
|
||||
bpy.ops.mesh.primitive_cube_add(size=1,location=p); o=bpy.context.object; o.name=n; o.dimensions=dims; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
|
||||
for col in list(o.users_collection): col.objects.unlink(o)
|
||||
c.objects.link(o); o.data.materials.append(mat); return o
|
||||
F=1.12
|
||||
# Explicit wall segments preserve a real, unobstructed doorway without boolean ambiguity.
|
||||
o=bpy.data.objects.get('Insulated end wall')
|
||||
if o: bpy.data.objects.remove(o,do_unlink=True)
|
||||
for ya,yb,zlo in [(-3,-2.365,F),(-1.235,3,F),(-2.365,-1.235,F+2.34)]:
|
||||
def top(y): return F+2.7+(y+3)*.7/6
|
||||
vs=[(x,y,z) for x in [-6.09,-5.91] for y,z in [(ya,zlo),(yb,zlo),(yb,top(yb)),(ya,top(ya))]]
|
||||
me=bpy.data.meshes.new('Living west wall with clear doorway'); me.from_pydata(vs,[],[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)]); me.materials.append(clay); ob=bpy.data.objects.new('Living west wall | clear corridor opening',me); c.objects.link(ob)
|
||||
o=bpy.data.objects.get('Short corridor north clay wall')
|
||||
if o: bpy.data.objects.remove(o,do_unlink=True)
|
||||
box('Corridor north wall below high window',(-7.75,-1.16,F+.895),(3.5,.12,1.79),clay)
|
||||
box('Corridor north wall above high window',(-7.75,-1.16,F+2.425),(3.5,.12,.15),clay)
|
||||
for x in [-9.31,-6.19]: box('Corridor north wall window end',(x,-1.16,F+2.07),(.38,.12,.56),clay)
|
||||
o=bpy.data.objects['Bedroom privacy partition south']; o.location.y=-5.61; o.dimensions.y=3.58
|
||||
o=bpy.data.objects['Bedroom privacy partition north return']; o.location.y=-2.57; o.dimensions.y=.34
|
||||
for x in [-9.2,-8.22]: box('Bathroom solid door jamb',(x,-3.85,F+1.14),(.05,.18,2.28),wood)
|
||||
# Close the sloping wall/roof junctions on each future child's enclosed room.
|
||||
for cx,cy in [(-11,12),(-3,17),(7,15)]:
|
||||
for xx in [cx-2.25,cx+2.25]:
|
||||
ya,yb=cy-2.4,cy+2.4
|
||||
vs=[(x,y,z) for x in [xx-.085,xx+.085] for y,z in [(ya,F+2.60),(yb,F+2.60),(yb,F+3.22),(ya,F+2.87)]]
|
||||
me=bpy.data.meshes.new('Sloping sealed child wall top'); me.from_pydata(vs,[],[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)]); me.materials.append(clay); ob=bpy.data.objects.new('Child room sealed sloping wall top',me); c.objects.link(ob)
|
||||
box('Child room north wall roof junction',(cx,cy+2.4,F+3.10),(4.5,.17,.22),clay)
|
||||
box('Child room south wall roof junction',(cx,cy-2.4,F+2.78),(4.5,.17,.17),clay)
|
||||
for ob in s.objects:
|
||||
if ob.type=='MESH' and len(ob.data.vertices)==8 and len(ob.data.polygons)==6:
|
||||
bm=bmesh.new(); bm.from_mesh(ob.data); bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces)); bm.to_mesh(ob.data); bm.free()
|
||||
if ob.type=='MESH' and ob.name.startswith(('Eucalyptus individual canopy leaves','Mango / avocado individual canopy leaves')): ob.hide_set(False)
|
||||
cam=s.camera; data=cam.data
|
||||
for f,pos,target in [(601,(-6.6,-1.8,2.70),(-8.9,-2.05,2.13)),(720,(-7.7,-1.8,2.65),(-8.9,-2.7,2.10))]:
|
||||
cam.location=pos; 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)
|
||||
# Update saved viewport state for Play and retain the leafy canopy in the interactive scene.
|
||||
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='SOLID'; area.spaces.active.shading.color_type='MATERIAL'; area.spaces.active.overlay.show_overlays=False
|
||||
s['Viewport performance']='Material-colour camera tour. Dense grass is hidden only in the viewport; canopy remains visible. Glazing uses outlines in solid viewport and renders as glass in Cycles.'
|
||||
# Check the doorway's centre ray reaches the corridor interior, not a wall at its threshold.
|
||||
dg=bpy.context.evaluated_depsgraph_get(); start=Vector((-5.5,-1.8,2.3)); hit=s.ray_cast(dg,start,Vector((-1,0,0)),distance=4.2)
|
||||
assert not hit[0] or hit[1].x<-7.9, (hit[4].name,hit[1][:])
|
||||
report=json.load(open(os.path.join(OUT,'tour-checks.json'))); report['doorway_clear']=True
|
||||
positions=[]
|
||||
for ch in json.load(open(os.path.join(OUT,'tour-chapters.json'))):
|
||||
s.frame_set(ch['start']); a=list(cam.location); s.frame_set(ch['end']); b=list(cam.location); assert (Vector(a)-Vector(b)).length>.01; positions.append({'chapter':ch['title'],'start_camera':a,'end_camera':b})
|
||||
report['all_chapters_have_camera_motion']=True; report['render_resolution']=[2560,1440]; json.dump(report,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
|
||||
s.frame_set(1); s.render.engine='CYCLES'; s.render.resolution_percentage=100; s.cycles.samples=40; s.cycles.use_denoising=True; s.cycles.adaptive_threshold=.045
|
||||
prefs=bpy.context.preferences.addons['cycles'].preferences; prefs.compute_device_type='METAL'; prefs.get_devices()
|
||||
for dev in prefs.devices: dev.use=dev.type=='METAL'
|
||||
s.cycles.device='GPU'; s.render.filepath=os.path.join(OUT,'family-homestead-overview.png')
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
|
||||
bpy.ops.render.render(write_still=True)
|
||||
print('FAMILY_OVERVIEW_RENDERED',flush=True)
|
||||
s.frame_set(2460); s.render.filepath=os.path.join(OUT,'three-childrens-rooms.png'); bpy.ops.render.render(write_still=True)
|
||||
print('CHILDREN_COURTYARD_RENDERED',flush=True)
|
||||
s.frame_set(2160); s.render.filepath=os.path.join(OUT,'child-room-interior.png'); bpy.ops.render.render(write_still=True)
|
||||
print('CHILD_ROOM_INTERIOR_RENDERED',flush=True)
|
||||
# One last fast screenshot verifies the revised short-corridor chapter.
|
||||
for ob in s.objects:
|
||||
if ob.name=='Subtle atmosphere over hillside': ob.hide_render=True
|
||||
s.frame_set(660); s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.color_type='MATERIAL'; s.render.filepath=os.path.join(OUT,'corridor-check.png'); bpy.ops.render.render(write_still=True)
|
||||
print('FINAL_TOUR_VERIFIED',flush=True)
|
||||
6
source/samford-spacious-tour/inspect_scene.py
Normal file
6
source/samford-spacious-tour/inspect_scene.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import bpy,json
|
||||
from mathutils import Vector
|
||||
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
|
||||
report={c.name:[{'name':o.name,'center':[round(v,2) for v in center(o)],'location':[round(v,2) for v in o.location],'dimensions':[round(v,2) for v in o.dimensions]} for o in c.objects if o.type in ['MESH','CAMERA','LIGHT','FONT']] for c in bpy.data.collections}
|
||||
json.dump(report,open('/tmp/samford-scene-inventory.json','w'),indent=2)
|
||||
print({c.name:len(c.objects) for c in bpy.data.collections})
|
||||
114
source/samford-spacious-tour/refine_adjacent_bedroom.py
Normal file
114
source/samford-spacious-tour/refine_adjacent_bedroom.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import bpy, math, os, json
|
||||
from mathutils import Vector
|
||||
from math import sin,cos,pi
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene
|
||||
def center(o): return o.matrix_world@(sum((Vector(v) for v in o.bound_box),Vector())/8)
|
||||
def ground(x,y): return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
|
||||
d=Vector((7.2,10.6,0))
|
||||
for name in ['02 Sleeping pavilion 8 x 5 m','21 Bathroom fittings']:
|
||||
for o in bpy.data.collections[name].objects:
|
||||
if name.startswith('21') and o.type=='LIGHT' and o.name.startswith('Living'): continue
|
||||
o.location+=d
|
||||
for o in bpy.data.collections['18 Starlink and single-server homelab'].objects:
|
||||
if o.name.startswith('Starlink'): o.location+=d
|
||||
for o in bpy.data.collections['19 Clay finishes and hemp wall infill'].objects:
|
||||
if o.name.startswith('Sleeping'): o.location+=d
|
||||
for o in bpy.data.collections['12 Natural materials and reversible connections'].objects:
|
||||
p=center(o)
|
||||
if o.name.startswith('Sleeping west expansion') or (o.name.startswith('Stone facing bed joint') and p.x<-16 and p.y<-11): o.location+=d
|
||||
for o in bpy.data.collections['05 Water and services'].objects:
|
||||
if o.name.startswith('Exposed rainwater downpipe') and center(o).x<-15: o.location+=d
|
||||
for o in list(bpy.data.collections['20 Spacious site connections'].objects):
|
||||
if any(o.name.startswith(n) for n in ['Extended','Walkway','Sleeping pavilion garden walk']): bpy.data.objects.remove(o,do_unlink=True)
|
||||
elif o.name.startswith('Optional extension link') and center(o).x<0: bpy.data.objects.remove(o,do_unlink=True)
|
||||
# Main common room remains undivided; the private bedroom becomes fully enclosed.
|
||||
for o in list(bpy.data.collections['02 Sleeping pavilion 8 x 5 m'].objects):
|
||||
if o.name=='Bedroom bathroom partition': bpy.data.objects.remove(o,do_unlink=True)
|
||||
COL={'room':bpy.data.collections['21 Bathroom fittings'],'link':bpy.data.collections['20 Spacious site connections']}; active='link'
|
||||
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']; roof=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']; F=1.12
|
||||
# Short enclosed link: west door of central room to north entrance of bedroom wing.
|
||||
box('SHORT CORRIDOR - 3.5m long x 1.2m wide',(-7.75,-1.8,F-.08),(3.5,1.2,.16),wood,.012)
|
||||
box('Short corridor insulated roof',(-7.75,-1.8,F+2.52),(3.65,1.37,.10),roof,.008)
|
||||
box('Short corridor north clay wall',(-7.75,-1.16,F+1.25),(3.5,.12,2.50),clay,.005)
|
||||
box('Short corridor south clay wall',(-6.95,-2.44,F+1.25),(1.60,.12,2.50),clay,.005)
|
||||
box('Short corridor west return',(-9.56,-1.8,F+1.25),(.12,1.4,2.50),clay,.005)
|
||||
for x in [-9.4,-6.2]:
|
||||
for y in [-2.31,-1.29]:
|
||||
g=ground(x,y); box('Short corridor stone pier',(x,y,(g+F-.16)/2),(.25,.25,F-.16-g),stone,.01)
|
||||
box('Short corridor exposed post',(x,y,F+1.25),(.085,.085,2.50),woodz,.005)
|
||||
for x in [-9.2,-8.4,-7.6,-6.8]: box('Short corridor exposed ceiling rafter',(x,-1.8,F+2.46),(.06,1.2,.12),wood)
|
||||
# High glazing in north wall admits daylight while preserving privacy.
|
||||
cut=box('TEMP corridor high light cutter',(-7.75,-1.16,F+2.07),(2.65,.5,.55),None)
|
||||
wall=bpy.data.objects['Short corridor north clay wall']; mod=wall.modifiers.new('Clerestory opening','BOOLEAN'); mod.object=cut; mod.operation='DIFFERENCE'; bpy.context.view_layer.objects.active=wall; bpy.ops.object.modifier_apply(modifier=mod.name); bpy.data.objects.remove(cut,do_unlink=True)
|
||||
win=box('Short corridor high daylight glazing',(-7.75,-1.16,F+2.07),(2.61,.018,.51),glass); win.display_type='WIRE'
|
||||
for z in [F+1.80,F+2.34]: box('Corridor clerestory timber rail',(-7.75,-1.16,z),(2.73,.08,.055),wood)
|
||||
# Privacy doors to two separate rooms, both reached from a shared entrance lobby.
|
||||
active='room'
|
||||
box('Bedroom privacy partition south',(-9.6,-5.79,F+1.34),(.15,3.22,2.68),clay,.006)
|
||||
box('Bedroom privacy partition north return',(-9.6,-2.51,F+1.34),(.15,.22,2.68),clay,.006)
|
||||
box('Bedroom privacy door lintel',(-9.6,-3.29,F+2.50),(.15,1.35,.32),clay)
|
||||
door=box('BEDROOM - independent solid timber door',(-9.6,-3.28,F+1.13),(.05,1.02,2.26),wood,.008)
|
||||
for y in [-3.82,-2.74]: box('Bedroom privacy door jamb',(-9.6,y,F+1.16),(.19,.06,2.32),woodz,.006)
|
||||
rod('Bedroom door lever',(-9.66,-2.92,F+1.06),(-9.78,-2.92,F+1.06),.015,steel)
|
||||
box('Bathroom lobby partition east',(-7.99,-3.85,F+1.34),(.37,.14,2.68),clay)
|
||||
box('Bathroom lobby partition west',(-9.40,-3.85,F+1.34),(.40,.14,2.68),clay)
|
||||
box('Bathroom lobby partition header',(-8.72,-3.85,F+2.49),(1.02,.14,.34),clay)
|
||||
box('BATHROOM - separate door from lobby',(-8.71,-3.85,F+1.12),(.95,.05,2.24),wood,.006)
|
||||
rod('Bathroom door lever',(-8.38,-3.81,F+1.05),(-8.20,-3.81,F+1.05),.015,steel)
|
||||
# Relocate wet fixtures deeper than the shared lobby, keeping the toilet out of the common-room sightline.
|
||||
for o in COL['room'].objects:
|
||||
if o.name.startswith('Bathroom shower'):
|
||||
o.location.y-=1.15
|
||||
if o.name=='Bathroom mineral floor': o.location.y=-5.70; o.dimensions.y=3.18
|
||||
# A wardrobe buffers the bed from the bathroom plumbing wall.
|
||||
for y in [-6.55,-5.70,-4.85]:
|
||||
box('Bedroom wardrobe cabinet',(-10.06,y,F+1.12),(.52,.81,2.24),wood,.009)
|
||||
box('Bedroom wardrobe discreet handle',(-10.34,y+.27,F+1.15),(.028,.025,.20),steel,.005)
|
||||
# Move dining slightly into the visual centre without changing generous kitchen circulation.
|
||||
for o in bpy.data.collections['10 Furniture'].objects:
|
||||
if o.name.startswith(('Dining table','Timber dining chair','Chair leg','Chair back')): o.location.x+=.30
|
||||
# Replace long-route chapter with short lobby views and reposition bedroom/bathroom/Starlink shots.
|
||||
cam=bpy.data.objects['PLAY TOUR camera']; data=cam.data
|
||||
changes={
|
||||
5:('06 / SHORT CORRIDOR TO PRIVATE ROOMS',(-5.6,-1.8,2.78),(-6.05,-1.8,2.72),(-8.7,-1.8,2.1),(-8.9,-2.3,2.1),24),
|
||||
6:('07 / ADJACENT BEDROOM WING',(-1,9,8),(-3,7,7),(-10,-3.5,2),(-11,-4,2),34),
|
||||
7:('08 / ENCLOSED BEDROOM',(-10.9,-3.3,2.9),(-11.2,-3.5,2.75),(-13.9,-5.2,1.8),(-13.9,-5.4,1.8),27),
|
||||
8:('09 / BATHROOM - SEPARATE LOBBY DOOR',(-9.0,-4.15,2.85),(-8.98,-4.35,2.75),(-8.52,-6.4,1.65),(-8.52,-6.55,1.65),22),
|
||||
13:('14 / ROOFTOP SOLAR & STARLINK',(3,7,10),(0,6,9),(-4,-1,4.3),(-6,-1.5,4.3),35),
|
||||
}
|
||||
for i,(title,a,b,ta,tb,lens) in changes.items():
|
||||
start=i*120+1; end=(i+1)*120
|
||||
for f,p,t in [(start,a,ta),(end,b,tb)]:
|
||||
cam.location=p; cam.rotation_euler=(Vector(t)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.keyframe_insert('location',frame=f); cam.keyframe_insert('rotation_euler',frame=f); data.lens=lens; data.keyframe_insert('lens',frame=f)
|
||||
for marker in s.timeline_markers:
|
||||
if marker.frame==start: marker.name=title
|
||||
for o in bpy.data.collections['22 PLAY - guided camera tour'].objects:
|
||||
if o.type=='FONT' and o.name.startswith('%02d /'%(i+1)):
|
||||
o.data.body=title; o.name=title; half=36/lens; o.location=(-half*.91,half*9/16*.84,-2)
|
||||
# Keep caption viewport state; disable the misleading legacy long-gap description.
|
||||
s['Spacious layout']='Final revision: central kitchen/dining/living pavilion; enclosed adjacent bedroom wing via a short 3.5m x 1.2m corridor and shared lobby. Bathroom has its own door from lobby. Sauna, tanks, garden and future dwellings remain spread out.'
|
||||
s['Adjoining pavilion explanation']='Adjacent bedroom wing contains a private bedroom, a shared entrance lobby and separately accessed bathroom. It is not a detached toilet block.'
|
||||
s['Architecture rationale']='North-facing common room and veranda retained; short circulation; solid privacy doors; wardrobe between bed and wet-service wall; existing openable facade retained. Site-specific structural, moisture, fire and access design still required.'
|
||||
for name in ['Sleeping ceiling light','Bathroom ceiling light']:
|
||||
o=bpy.data.objects.get(name)
|
||||
if o: o.location.y-=.6
|
||||
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_set(1); s.camera=cam; s.render.engine='CYCLES'; s.render.resolution_percentage=100
|
||||
checks=json.load(open(os.path.join(OUT,'tour-checks.json'))); checks.pop('north_south_clear_gap_metres',None); checks.update({'sleeping_center':[-11.8,-4.9],'short_corridor_metres':[3.5,1.2],'bedroom_separate_room':True,'bathroom_access':'Own door from shared lobby; no passage through bedroom','layout_revision':'Adjacent private bedroom and central kitchen/dining; dispersed outbuildings'})
|
||||
json.dump(checks,open(os.path.join(OUT,'tour-checks.json'),'w'),indent=2)
|
||||
chapters=json.load(open(os.path.join(OUT,'tour-chapters.json')))
|
||||
for i,v in changes.items(): chapters[i]['title']=v[0]
|
||||
json.dump(chapters,open(os.path.join(OUT,'tour-chapters.json'),'w'),indent=2)
|
||||
txt=bpy.data.texts['START HERE - PRESS PLAY']; txt.clear(); txt.write(s['PLAY THE TOUR']+'\n\n'+s['Spacious layout']+'\n\n'+s['Adjoining pavilion explanation']+'\n\n'+s['Viewport performance'])
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
|
||||
# Review every chapter with the final layout.
|
||||
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(20):
|
||||
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('ADJACENT_BEDROOM_TOUR_REVIEW_READY',flush=True)
|
||||
24
source/samford-spacious-tour/save_ready_to_play.py
Normal file
24
source/samford-spacious-tour/save_ready_to_play.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import bpy,os
|
||||
from mathutils import Vector
|
||||
OUT=os.path.dirname(os.path.abspath(__file__)); s=bpy.context.scene; cam=s.camera
|
||||
# A wider architectural view makes the short link easier to understand than a close doorway shot.
|
||||
for f,pos,target in [(601,(-13,3.0,5.7),(-8,-1.8,2.6)),(720,(-12,2.4,5.1),(-8,-1.8,2.6))]:
|
||||
cam.location=pos; 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)
|
||||
# Six seats let the whole family gather around the central table.
|
||||
c=bpy.data.collections['10 Furniture']; wood=bpy.data.materials['Unpainted hardwood - long X grain']
|
||||
def box(n,p,sz):
|
||||
bpy.ops.mesh.primitive_cube_add(size=1,location=p); o=bpy.context.object; o.name=n; o.dimensions=sz; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
|
||||
for col in list(o.users_collection): col.objects.unlink(o)
|
||||
c.objects.link(o); o.data.materials.append(wood); b=o.modifiers.new('Eased timber edge','BEVEL'); b.width=.007; b.segments=2
|
||||
for x,side in [(-1.46,-1),(1.66,1)]:
|
||||
box('Family dining end chair seat',(x,.3,1.55),(.46,.46,.045))
|
||||
for dx in [-.17,.17]:
|
||||
for dy in [-.17,.17]: box('Family dining end chair leg',(x+dx,.3+dy,1.33),(.038,.038,.42))
|
||||
box('Family dining end chair back',(x+side*.2,.3,1.82),(.035,.46,.32))
|
||||
s.frame_set(1); s.camera=cam; s.render.engine='CYCLES'; s.render.resolution_percentage=100; s.render.filepath=os.path.join(OUT,'family-homestead-overview.png')
|
||||
s['Family table']='Six dining seats around the shared central table.'
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'samford-spacious-guided-tour.blend'))
|
||||
for o in s.objects:
|
||||
if o.name=='Subtle atmosphere over hillside': o.hide_render=True
|
||||
s.frame_set(660); s.render.engine='BLENDER_WORKBENCH'; s.render.resolution_percentage=30; s.display.shading.color_type='MATERIAL'; s.render.filepath=os.path.join(OUT,'corridor-check.png'); bpy.ops.render.render(write_still=True)
|
||||
print('READY_TO_PLAY_SAVED',flush=True)
|
||||
134
source/samford-spacious-tour/tour-chapters.json
Normal file
134
source/samford-spacious-tour/tour-chapters.json
Normal file
@@ -0,0 +1,134 @@
|
||||
[
|
||||
{
|
||||
"title": "01 / THE SPACIOUS HOMESTEAD",
|
||||
"start": 1,
|
||||
"end": 120,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "02 / LIVING PAVILION & VERANDA",
|
||||
"start": 121,
|
||||
"end": 240,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "03 / KITCHEN & EXPOSED STRUCTURE",
|
||||
"start": 241,
|
||||
"end": 360,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "04 / DINING & OPEN LIVING",
|
||||
"start": 361,
|
||||
"end": 480,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "05 / ONE SERVER - LOCAL AI",
|
||||
"start": 481,
|
||||
"end": 600,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "06 / SHORT CORRIDOR TO PRIVATE ROOMS",
|
||||
"start": 601,
|
||||
"end": 720,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "07 / ADJACENT BEDROOM WING",
|
||||
"start": 721,
|
||||
"end": 840,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "08 / ENCLOSED BEDROOM",
|
||||
"start": 841,
|
||||
"end": 960,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "09 / BATHROOM - SEPARATE LOBBY DOOR",
|
||||
"start": 961,
|
||||
"end": 1080,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "10 / FINNISH SAUNA & OUTDOOR SHOWER",
|
||||
"start": 1081,
|
||||
"end": 1200,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "11 / SAUNA BENCHES & STONE HEATER",
|
||||
"start": 1201,
|
||||
"end": 1320,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "12 / OUTDOOR SHOWER",
|
||||
"start": 1321,
|
||||
"end": 1440,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "13 / TWO RAINWATER TANKS",
|
||||
"start": 1441,
|
||||
"end": 1560,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "14 / ROOFTOP SOLAR & STARLINK",
|
||||
"start": 1561,
|
||||
"end": 1680,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "15 / SOLAR ARRAY & ENERGY STORAGE",
|
||||
"start": 1681,
|
||||
"end": 1800,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "16 / FOOD GARDEN & GREENHOUSE",
|
||||
"start": 1801,
|
||||
"end": 1920,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "17 / INSIDE THE GREENHOUSE",
|
||||
"start": 1921,
|
||||
"end": 2040,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "18 / CHILD 1 - THEIR OWN ROOM",
|
||||
"start": 2041,
|
||||
"end": 2160,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "19 / CHILD 2 - THEIR OWN ROOM",
|
||||
"start": 2161,
|
||||
"end": 2280,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "20 / CHILD 3 - THEIR OWN ROOM",
|
||||
"start": 2281,
|
||||
"end": 2400,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "21 / THREE PRIVATE ROOMS - ONE FAMILY",
|
||||
"start": 2401,
|
||||
"end": 2520,
|
||||
"seconds": 5
|
||||
},
|
||||
{
|
||||
"title": "22 / LIVE FIRST. GROW TOGETHER.",
|
||||
"start": 2521,
|
||||
"end": 2640,
|
||||
"seconds": 5
|
||||
}
|
||||
]
|
||||
34
source/samford-spacious-tour/tour-checks.json
Normal file
34
source/samford-spacious-tour/tour-checks.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"duration_seconds": 110,
|
||||
"frames": 2640,
|
||||
"shots": 22,
|
||||
"tank_count": 2,
|
||||
"solar_panels": 30,
|
||||
"garden_beds": 6,
|
||||
"servers": 1,
|
||||
"sleeping_center": [
|
||||
-11.8,
|
||||
-4.9
|
||||
],
|
||||
"active_camera": "PLAY TOUR camera",
|
||||
"bathroom_fitted": true,
|
||||
"short_corridor_metres": [
|
||||
3.5,
|
||||
1.2
|
||||
],
|
||||
"bedroom_separate_room": true,
|
||||
"bathroom_access": "Own door from shared lobby; no passage through bedroom",
|
||||
"layout_revision": "Adjacent private bedroom and central kitchen/dining; dispersed outbuildings",
|
||||
"future_child_rooms": 3,
|
||||
"child_room_footprints": [
|
||||
4.5,
|
||||
4.8
|
||||
],
|
||||
"generic_future_office_guest_workshop_removed": true,
|
||||
"doorway_clear": true,
|
||||
"all_chapters_have_camera_motion": true,
|
||||
"render_resolution": [
|
||||
2560,
|
||||
1440
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user