"""Photo-derived exterior. Run in Blender; rebuild only this project's owned scene/data.""" import bpy, math, json from mathutils import Vector from math import sin,cos,pi PREFIX='ATHR_' BASE_SECTIONS=['shell','portico','elevations','roofs','crown'] REAR_SECTIONS=['rear_body','rear_roof','roof_equipment','rear_facade','dome_roof'] SECTIONS=BASE_SECTIONS+REAR_SECTIONS PROGRESS={k:2 for k in REAR_SECTIONS} COLORS={'plaster':(.72,.66,.53,1),'stone':(.83,.78,.66,1),'zinc':(.19,.22,.24,1),'seam':(.12,.15,.16,1),'glass':(.045,.085,.1,1),'wood':(.25,.105,.035,1),'gold':(.63,.42,.12,1),'dark':(.05,.045,.036,1),'roof_metal':(.48,.52,.56,1),'equipment':(.55,.57,.57,1),'grille':(.13,.15,.16,1),'rear_plaster':(.66,.64,.58,1)} def scene(): s=bpy.data.scenes.get('AthenaeumRear') if s is None: s=bpy.data.scenes.new('AthenaeumRear');s.unit_settings.system='METRIC';s.unit_settings.scale_length=1 s.world=bpy.data.worlds.new(PREFIX+'World');s.world.use_nodes=True bg=next(n for n in s.world.node_tree.nodes if n.type=='BACKGROUND');bg.inputs[0].default_value=(.53,.63,.75,1);bg.inputs[1].default_value=.55 try:s.render.engine='CYCLES' except TypeError as e:raise RuntimeError(str(e)) s.cycles.samples=24;s.cycles.use_denoising=True;s.render.resolution_x=1200;s.render.resolution_y=1000;s.render.resolution_percentage=100 s.render.image_settings.file_format='PNG' s.view_settings.view_transform='AgX' if bpy.context.window:bpy.context.window.scene=s return s def mat(key): name=PREFIX+key;m=bpy.data.materials.get(name) if m is None: m=bpy.data.materials.new(name);m.use_nodes=True;m.diffuse_color=COLORS[key] n=next(n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED');n.inputs['Base Color'].default_value=COLORS[key];n.inputs['Roughness'].default_value=.76 if key in ['stone','plaster'] else .4 n.inputs['Metallic'].default_value=.65 if key in ['gold','zinc','seam'] else 0 return m def Geo():return {'v':[],'f':[],'m':[],'smooth':[]} def geo_add(g,v,f,m='stone',smooth=False): k=len(g['v']);g['v'].extend(v);g['f'].extend([tuple(k+i for i in face) for face in f]);g['m'].extend([m]*len(f));g['smooth'].extend([smooth]*len(f)) def geo_box(g,c,d,m='stone'): x,y,z=c;a,b,h=[i/2 for i in d] geo_add(g,[(x+i*a,y+j*b,z+k*h) for k in [-1,1] for j in [-1,1] for i in [-1,1]],[(0,2,3,1),(4,5,7,6),(0,1,5,4),(2,6,7,3),(0,4,6,2),(1,3,7,5)],m) def geo_prism(g,outline,y0,y1,m='stone'): # outline is counterclockwise in local X/Z; extrusion has closed ends. clean=[] for pt in outline: if not clean or sum((pt[i]-clean[-1][i])**2 for i in range(2))>1e-16:clean.append(pt) if len(clean)>2 and sum((clean[0][i]-clean[-1][i])**2 for i in range(2))<1e-16:clean.pop() assert len(clean)>=3 area=sum(clean[i][0]*clean[(i+1)%len(clean)][1]-clean[(i+1)%len(clean)][0]*clean[i][1] for i in range(len(clean))) outline=clean if area>0 else list(reversed(clean));n=len(outline);v=[(x,y,z) for y in [y0,y1] for x,z in outline] geo_add(g,v,[tuple(range(n)),tuple(range(2*n-1,n-1,-1))]+[(i+n,(i+1)%n+n,(i+1)%n,i) for i in range(n)],m) def geo_lathe(g,c,profile,m='stone',n=64,flutes=0): x,y,z=c;v=[] for r,h in profile: for j in range(n): a=j*2*pi/n;rr=r-(.045*(1+cos(flutes*a))/2 if flutes and r>.35 else 0);v.append((x+rr*cos(a),y+rr*sin(a),z+h)) f=[(k*n+j,k*n+(j+1)%n,(k+1)*n+(j+1)%n,(k+1)*n+j) for k in range(len(profile)-1) for j in range(n)] capped=profile[0]!=profile[-1] if capped:f+=[tuple(range(n-1,-1,-1)),tuple((len(profile)-1)*n+j for j in range(n))] geo_add(g,v,f,m,True) if capped:g['smooth'][-2:]=[False,False] def geo_tube(g,pts,r=.05,m='stone',n=8,closed=False): v=[];count=len(pts);previous_q=None for i,p in enumerate(pts): t=Vector(pts[(i+1)%count])-Vector(pts[i-1 if i else (count-1 if closed else 0)]) if not closed and i==count-1:t=Vector(pts[-1])-Vector(pts[-2]) t.normalize();q=t.cross(Vector((0,0,1))) if q.length<.1:q=t.cross(Vector((0,1,0))) q.normalize() if previous_q is not None and q.dot(previous_q)<0:q.negate() previous_q=q.copy();w=t.cross(q).normalized() for j in range(n):v.append(tuple(Vector(p)+r*(cos(j*2*pi/n)*q+sin(j*2*pi/n)*w))) f=[(i*n+j,i*n+(j+1)%n,((i+1)%count)*n+(j+1)%n,((i+1)%count)*n+j) for i in range(count if closed else count-1) for j in range(n)] if not closed:f += [tuple(range(n-1,-1,-1)),tuple((count-1)*n+j for j in range(n))] geo_add(g,v,f,m,True) def geo_ring(g,c,r,t=.06,m='stone',a0=0,a1=2*pi): x,y,z=c;geo_tube(g,[(x+r*cos(a0+(a1-a0)*i/64),y,z+r*sin(a0+(a1-a0)*i/64)) for i in range(64 if a1-a0==2*pi else 65)],t,m,8,a1-a0==2*pi) def geo_beam(g,a,b,w,d=None,m='stone'): # Tube with square section; stable radial basis keeps seam framing consistent. a,b=Vector(a),Vector(b);t=(b-a).normalized();q=t.cross(Vector((0,1,0))) if q.length<.1:q=t.cross(Vector((0,0,1))) q.normalize();v=t.cross(q).normalized();d=d or w verts=[tuple(p+q*i*w/2+v*j*d/2) for p in [a,b] for i,j in [(-1,-1),(1,-1),(1,1),(-1,1)]] geo_add(g,verts,[(3,2,1,0),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)],m) def geo_finish(g,name,sec,loc=(0,0,0),rot=0): me=bpy.data.meshes.new(PREFIX+name);me.from_pydata(g['v'],[],g['f']);me.update();keys=list(dict.fromkeys(g['m'])) for k in keys:me.materials.append(mat(k)) for p,m,sm in zip(me.polygons,g['m'],g['smooth']):p.material_index=keys.index(m);p.use_smooth=sm uv=me.uv_layers.new(name='Metres_2m') for p in me.polygons: normal=p.normal;axis=list(map(abs,normal)).index(max(map(abs,normal)));axes=[a for a in range(3) if a!=axis] for li in p.loop_indices: co=me.vertices[me.loops[li].vertex_index].co;uv.data[li].uv=(co[axes[0]]/2,co[axes[1]]/2) ob=bpy.data.objects.new(PREFIX+name,me);collection(sec).objects.link(ob);ob.location=loc;ob.rotation_euler.z=rot;ob['section']=sec;ob['photo_derived']=True;return ob def collection(sec): name=PREFIX+sec;c=bpy.data.collections.get(name) if c is None:c=bpy.data.collections.new(name);scene().collection.children.link(c) return c def instance(ob,name,sec,loc,rot=0): o=bpy.data.objects.new(PREFIX+name,ob.data);collection(sec).objects.link(o);o.location=loc;o.rotation_euler.z=rot;o['section']=sec;return o def clear_section(sec): c=collection(sec) for o in list(c.objects):bpy.data.objects.remove(o,do_unlink=True) for m in list(bpy.data.meshes): if m.name.startswith(PREFIX) and m.users==0:bpy.data.meshes.remove(m) for c in list(bpy.data.curves): if c.name.startswith(PREFIX) and c.users==0:bpy.data.curves.remove(c) def slabpoly(g,xy,z0,z1,m='stone'): n=len(xy);geo_add(g,[(x,y,z) for z in [z0,z1] for x,y in xy],[tuple(range(n-1,-1,-1)),tuple(range(n,2*n))]+[(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)],m) def molding(g,c,w,depth,zs,m='stone'): for z,h,proj in zs:geo_box(g,(c[0],c[1],z),(w+2*proj,depth+2*proj,h),m) def column(g,x,y,z,h,r=.64,ionic=True,level=0): geo_lathe(g,(x,y,z),[(r*1.5,0),(r*1.5,.16),(r*1.35,.22),(r*1.15,.34),(r*1.28,.43),(r*1.1,.55),(r,.7),(r*.9,h-1.05),(r*1.16,h-.87),(r*1.2,h-.65)],n=96 if level else 48,flutes=24 if level else 0) geo_box(g,(x,y,z+h-.17),(r*2.9,r*2.5,.34)) if ionic: for s in [-1,1]: cx=x+s*r*.97;cy=y-r*.9;cz=z+h-.55 pts=[] for j in range(80): a=j/79*pi*3.8;rr=r*.55*(1-j/90);pts.append((cx+rr*cos(a),cy,cz+rr*sin(a))) geo_tube(g,pts,.045 if level else .065) if level>=2: for j in range(9):geo_ring(g,(x+(j-4)*r*.19,y-r*.97,z+h-.74),r*.065,.025) else: for row in range(2 if level else 1): for j in range(8): a=j*pi/4;px=x+r*.85*cos(a);py=y+r*.85*sin(a);pts=[(px,py,z+h-1.1+row*.32),(x+r*1.4*cos(a),y+r*1.4*sin(a),z+h-.65+row*.2),(x+r*1.25*cos(a),y+r*1.25*sin(a),z+h-.43)] geo_tube(g,pts,r*.12) def shell(level): g=Geo();foot=[(-23.8,-19.3),(23.8,-19.3),(23.8,25.5),(-23.8,34.5)] slabpoly(g,foot,0,.65) # No solid block behind the windows: facades are open assemblies around glazed apertures. geo_lathe(g,(0,0,.65),[(14.8,0),(14.8,17.2)],'plaster',128) geo_box(g,(0,0,1),(46,35.5,.5));geo_box(g,(0,0,15.6),(46,35.5,.6),'plaster') if level>=3: # Fine base rustication belongs to the plinth, not every plaster tile. for z in [1,1.5,2]: geo_box(g,(0,-19.24,z),(47.2,.08,.04),'plaster') geo_finish(g,'Podium body and courtyard','shell') def portico(level): g=Geo() for j in range(7): front=-28.4+j*.39;h=(j+1)*.25;geo_box(g,(0,(front-18)/2,h/2),(30.6-j*.18,-18-front,h)) geo_box(g,(0,-21.7,1.82),(29.3,7.5,.3)) c=Geo();column(c,0,0,1.98,12,.69,True,level);proto=geo_finish(c,'Ionic column01','portico',(-12.25,-24.2,0)) for i,x in enumerate([-7.35,-2.45,2.45,7.35,12.25]):instance(proto,f'Ionic column{i+2:02}','portico',(x,-24.2,0)) for i,x in enumerate([-12.25,12.25]):instance(proto,f'Ionic return{i+1}','portico',(x,-20.4,0)) for z,h,w,d in [(14.1,.32,29,7.5),(14.65,.8,29.3,7.6),(15.2,.25,29.8,8),(15.5,.27,30.1,8.1)]:geo_box(g,(0,-21,z),(w,d,h)) geo_prism(g,[(-15,15.64),(15,15.64),(0,20.15)],-25.15,-17.4,'plaster') for y in [-25.25,-17.3]: for off in [0,.18,.36]: geo_beam(g,(-15-off,y,15.58+off),(0,y,20.15+off),.16,.25);geo_beam(g,(0,y,20.15+off),(15+off,y,15.58+off),.16,.25) # Recessed entrance wall divided around three doors. geo_box(g,(0,-18.45,12),(28.5,.8,4.2),'plaster');geo_box(g,(0,-18.45,2.35),(28.5,.8,.9),'plaster') edges=[(-14.25,-8.7),(-5.7,-1.5),(1.5,5.7),(8.7,14.25)] for a,b in edges:geo_box(g,((a+b)/2,-18.45,6.35),(b-a,.8,7.1),'plaster') for x in [-7.2,0,7.2]: geo_box(g,(x,-18.2,6.25),(2.95,.25,6.5),'wood');geo_box(g,(x,-18.39,7.4),(2.45,.12,3.5),'glass') for dx in [-1.65,1.65]:geo_box(g,(x+dx,-18.96,6.3),(.22,.35,6.8)) for z in [3,6.1,8.8,9.7]:geo_box(g,(x,-18.64,z),(3.3,.3,.18)) for dx in [-.7,0,.7]:geo_box(g,(x+dx,-18.55,6.25),(.1,.14,6.4),'wood') for i,x in enumerate([-9.8,-4.9,0,4.9,9.8]): geo_ring(g,(x,-18.98,11.68),.92,.13,'gold');geo_ring(g,(x,-19,11.68),.78,.065,'gold') # shortcut: generalized portrait relief; trace individual mosaics when exact likeness is required. geo_prism(g,[(x-.78,11.68),(x,10.9),(x+.78,11.68),(x,12.46)],-18.91,-18.87,'gold') if level: geo_lathe(g,(x,-19.02,11.54),[(.2,0),(.2,.5)],'dark',20) geo_prism(g,[(x-.42,11.07),(x+.42,11.07),(x+.25,11.47),(x-.25,11.47)],-19.07,-19.02,'dark') if level: for j in range(87):geo_box(g,(-14.75+j*.343,-25.08,15.22),(.15,.22,.23)) if level>=2: for x in [-9.8,-4.9,0,4.9,9.8]: geo_box(g,(x,-18.94,10.25),(3.6,.1,.16));geo_box(g,(x,-18.94,13.35),(3.6,.1,.16)) for x in [-14.5,14.5]: for j in range(4):geo_ring(g,(x,-25.2,16+j*.1),.14+j*.1,.05) if level>=3: geo_box(g,(0,-25.17,16.08),(.48,.1,.5),'plaster') geo_finish(g,'Entrance stairs entablature doors mosaics','portico') add_text('ATENEUL ROMÂN',(0,-25.19,14.55),.61,'portico',.018) def add_text(body,loc,size,sec,depth=.005,rot=0): cu=bpy.data.curves.new(PREFIX+body,'FONT');cu.body=body;cu.align_x='CENTER';cu.align_y='CENTER';cu.size=size;cu.space_character=1.2;cu.extrude=depth ob=bpy.data.objects.new(PREFIX+body,cu);collection(sec).objects.link(ob);ob.location=loc;ob.rotation_euler=(pi/2,0,rot);cu.materials.append(mat('stone'));ob['section']=sec return ob def rosette(g,x,y,z,r=.3,level=1): geo_ring(g,(x,y,z),r,.055) if level>=2: for j in range(12): a=j*pi/6;geo_tube(g,[(x+.08*cos(a),y-.02,z+.08*sin(a)),(x+r*.8*cos(a),y-.03,z+r*.8*sin(a))],.024) def bay(level,blind=False): g=Geo();w=6.5 # Solid piers and spandrels leave real window voids. for x in [-3.02,3.02]:geo_box(g,(x,.35,8.8),(.46,.85,13.6),'plaster') geo_box(g,(0,.35,1.55),(w,.85,2.1),'plaster');geo_box(g,(0,.35,9.4),(w,.85,1.1),'plaster');geo_box(g,(0,.35,15.15),(w,.85,1.5),'plaster') geo_box(g,(0,-.08,2.9),(w,.5,.35));geo_box(g,(0,-.16,15.7),(w+.1,.65,.35));geo_box(g,(0,-.08,16.05),(w+.3,.8,.26)) for x in [-1.68,0,1.68]: if not blind:geo_box(g,(x,.34,6.1),(1.38,.12,5.25),'glass') else:geo_box(g,(x,.26,6.1),(1.38,.16,5.25),'plaster') for dx in [-.72,.72]:geo_box(g,(x+dx,.12,6.1),(.09,.17,5.3),'wood') geo_box(g,(x,.1,6.7),(1.4,.18,.1),'wood') for x in [-2.65,2.65]:column(g,x,-.15,3.05,6.3,.23,True,level) # Lunette wall fills above the curved aperture, not behind glazing. radius=2.78;zb=10.05 for j in range(32): a=j*pi/32;b=(j+1)*pi/32 pts=[(radius*cos(a),zb+radius*sin(a)),(radius*cos(b),zb+radius*sin(b)),(radius*cos(b),14.5),(radius*cos(a),14.5)] geo_prism(g,pts,-.05,.8,'plaster') if not blind: arc=[(-radius,zb)]+[(radius*cos(pi-j*pi/40),zb+radius*sin(pi-j*pi/40)) for j in range(41)] geo_prism(g,arc,.2,.3,'glass') else:geo_prism(g,[(-radius,zb),(radius,zb)]+[(radius*cos(j*pi/40),zb+radius*sin(j*pi/40)) for j in range(41)],.2,.3,'plaster') for r in [2.83,2.98,3.08]:geo_ring(g,(0,-.17,zb),r,.07,'stone',0,pi) for x in [-1,1]:geo_box(g,(x,-.02,11.15),(.14,.3,2.3)) for x in [-3.1,3.1]:column(g,x,-.15,3.05,7,.36,False,level) if level: for x in [-2.2,0,2.2]:rosette(g,x,-.24,9.6,.2,level) for x in [-2.15,0,2.15]:rosette(g,x,-.14,1.6,.48,level) for j in range(16): a=j*pi/16;geo_beam(g,(2.84*cos(a),-.2,zb+2.84*sin(a)),(3.13*cos(a),-.2,zb+3.13*sin(a)),.025,.035,'plaster') if level>=2: for j in range(20):geo_box(g,(-3.1+j*.325,-.42,15.54),(.14,.24,.25)) # Eagle/foliate spandrel motif is generalized geometry from the photographed silhouette. for s in [-1,1]: pts=[(s*.1,-.24,13.45),(s*.65,-.25,13.8),(s*1.25,-.24,14.2),(s*1.7,-.2,14.32)] geo_tube(g,pts,.13) for j in range(5):geo_beam(g,(s*(.3+j*.23),-.24,13.55+j*.15),(s*(.5+j*.24),-.22,13.14+j*.17),.12,.07) if level>=3: for x in [-1.68,0,1.68]: if not blind: for z in [4.2,5.1,7.7,8.5]:geo_box(g,(x,.05,z),(1.35,.1,.055),'wood') for j in range(20): a=j*pi/10;geo_ring(g,(.5*cos(a),-.28,13.23+.3*sin(a)),.09,.035) return g def elevations(level): normal=geo_finish(bay(level),'Tripartite arched bay01','elevations',(23.5,-14.1,0),pi/2) normal.scale.x=6.1/6.5 for s in [-1,1]: count=7 if s==1 else 8 for j in range(count): if s==1 and j==0:continue ob=instance(normal,f'Arched side{s} bay{j}','elevations',(s*23.5,(-14.1+j*6.1) if s==1 else (-13.9+j*6.5),0),s*pi/2) if s==1:ob.scale.x=6.1/6.5 # Front pavilions: broad blind panels and upper open star lattice. g=Geo() for s in [-1,1]: geo_box(g,(s*23.2,-18.35,8.1),(.6,2.1,15.1),'plaster') x=s*19.6;geo_box(g,(x,-18.9,7.4),(7.6,.8,9.5),'plaster');geo_box(g,(x,-18.9,1.6),(7.6,.8,2.1),'plaster') for dx in [-3.65,3.65]:column(g,x+dx,-19.45,2.8,12.8,.32,True,level) for z,h in [(3,.25),(11.8,.35),(15.7,.35),(16.05,.25)]:geo_box(g,(x,-19.2,z),(8.5,1.1,h)) geo_box(g,(x,-19.23,13.6),(7.1,.35,2.7),'dark') for j in [-1,1]: cx=x+j*1.75 for dx in [-1.5,1.5]:geo_box(g,(cx+dx,-19.6,13.65),(.16,.25,2.4)) for z in [12.5,14.8]:geo_box(g,(cx,-19.6,z),(3.2,.25,.16)) for a in [0,pi/2,pi/4,-pi/4]: geo_beam(g,(cx-1.35*cos(a),-19.61,13.65-1.05*sin(a)),(cx+1.35*cos(a),-19.61,13.65+1.05*sin(a)),.17,.24) if level: for j in [-1,1]: xx=x+j*1.75;geo_box(g,(xx,-19.5,7.2),(1.45,.2,5.9),'wood');geo_box(g,(xx,-19.65,7.4),(1.15,.08,5.3),'glass') for dx in [-.85,.85]:geo_box(g,(xx+dx,-19.7,7.1),(.18,.3,6.4)) geo_box(g,(xx,-19.7,10.45),(2,.4,.27));rosette(g,xx,-19.7,11.15,.32,level) if level>=2: for z in [3.5,4.4,5.3,6.2,7.1,8,8.9,9.8,10.7]: for dx in [-4.1,4.1]:geo_box(g,(x+dx,-19.4,z),(.5,.3,.065),'plaster') geo_finish(g,'Front corner pavilions and lattice','elevations') def hip(g,x0,x1,y0,y1,z,rise,level): ridgey=(y0+y1)/2;ridgex=(x0+x1)/2;inset=min((x1-x0)/2,(y1-y0)/2) # Hip rectangle with ridge along longer direction. if y1-y0>x1-x0:r0=(ridgex,y0+inset,z+rise);r1=(ridgex,y1-inset,z+rise) else:r0=(x0+inset,ridgey,z+rise);r1=(x1-inset,ridgey,z+rise) v=[(x0,y0,z),(x1,y0,z),(x1,y1,z),(x0,y1,z),r0,r1] if y1-y0>x1-x0:f=[(0,1,4),(1,2,5,4),(2,3,5),(3,0,4,5)] else:f=[(0,1,5,4),(1,2,5),(2,3,4,5),(3,0,4)] # Intentional roofing sheets: two sides supported by solid walls below. geo_add(g,v,f,'zinc') for a,b in [(0,4),(1,4 if y1-y0>x1-x0 else 5),(2,5),(3,5 if y1-y0>x1-x0 else 4),(4,5)]:geo_tube(g,[v[a],v[b]],.055,'seam') if level: if y1-y0>x1-x0: for j in range(int((y1-y0)/.72)): y=y0+.4+j*.72 if y0+inset<=y<=y1-inset: for x in [x0,x1]:geo_tube(g,[(x,y,z+.02),(ridgex,y,z+rise+.02)],.018,'seam',6) else: for j in range(int((x1-x0)/.72)): x=x0+.4+j*.72 if x0+inset<=x<=x1-inset: for y in [y0,y1]:geo_tube(g,[(x,y,z+.02),(x,ridgey,z+rise+.02)],.018,'seam',6) def roofs(level): g=Geo() # Metal roofing on portico slopes leaves the plaster front pediment exposed. for side in [-1,1]: geo_add(g,[(side*15,-24.97,15.72),(0,-24.97,20.23),(0,-17.35,20.23),(side*15,-17.35,15.72)],[tuple(range(3,-1,-1)) if side==1 else (0,1,2,3)],'zinc') for s in [-1,1]: a,b=(15.1,24) if s==1 else (-24,-15.1);rear=17.7 hip(g,a,b,-19.6,rear,16.25,2.7,level) # Front and rear cross roofs kept below staircaps. Courtyard is not roofed over. hip(g,-15.1,15.1,-18.1,-11.5,16.2,2.5,level) hip(g,-15,15,12,17.7,16.2,2,level) for x,y in [(-12,-12),(12,-12),(-12,12),(12,12)]: geo_lathe(g,(x,y,16),[(3.5,0),(3.5,1.4),(3.65,1.5),(3.55,1.7)]) geo_lathe(g,(x,y,17.7),[(3.6,0),(3.1,.75),(2,1.5),(.5,2),(.05,2.1)],'zinc',64) if level: for j in range(24): a=j*pi/12;geo_tube(g,[(x+r*cos(a),y+r*sin(a),17.72+h) for r,h in [(3.6,0),(3.1,.75),(2,1.5),(.5,2)]],.022,'seam',6) if level>=2: for s in [-1,1]: y1=29.7-.19*s*19;geo_tube(g,[(s*24,-19.4,16.3),(s*24,y1,16.3)],.09,'seam') if level>=3: for s in [-1,1]:geo_tube(g,[(s*23.7,15.8,16.1),(s*23.7,15.8,.5)],.065,'seam') geo_finish(g,'Metal hip roofs and four stair caps','roofs') DOME=[(16.35,26.0),(16.4,26.4),(16.1,27.3),(15.5,28.5),(14.5,30),(13,31.6),(11.2,33.2),(9.1,34.6),(7,35.6),(5.6,36.1),(5.3,36.25)] def crown(level): g=Geo() # Drum shell is assembled between round apertures, preserving true window clearance. r=15.9 for j in range(20): a=-pi/2+j*2*pi/20;delta=pi/20 pts=[((r+.0)*cos(a-delta),(r+.0)*sin(a-delta)),((r+.0)*cos(a+delta),(r+.0)*sin(a+delta))] # Curved solid ring zones above/below apertures. for z0,z1 in [(18,20.35),(24,25.8)]:geo_lathe(g,(0,0,0),[(15.9,z0),(15.9,z1),(15.35,z1),(15.35,z0),(15.9,z0)],'plaster',160) for j in range(20): a=-pi/2+(j+.5)*2*pi/20 # Narrow pier separating oculi. Local modules fill the circular spandrel. geo_lathe(g,(0,0,0),[(16.05,18.1),(16.05,18.4)],'stone',160) if j==0 else None for r0,z,h in [(16.05,18,.25),(16.25,18.5,.25),(16.05,24.3,.23),(16.35,25.2,.25),(16.5,25.6,.35),(16.65,25.93,.25)]:geo_lathe(g,(0,0,z),[(r0,0),(r0,h),(r0-.3,h),(r0-.3,0),(r0,0)],n=160) # Local oculus module, outward -Y. Circular clear aperture radius1.48m. o=Geo();rr=1.48;zc=22.1;half=2.5 for k in range(64): a=k*2*pi/64;b=(k+1)*2*pi/64 def border(t): dx,dz=cos(t),sin(t);sc=min(half/max(abs(dx),1e-8),1.9/max(abs(dz),1e-8));return dx*sc,zc+dz*sc geo_prism(o,[(rr*cos(a),zc+rr*sin(a)),(rr*cos(b),zc+rr*sin(b)),border(b),border(a)],0,.55,'plaster') for rad,th in [(1.53,.12),(1.7,.09),(1.86,.065)]:geo_ring(o,(0,-.13,zc),rad,th) # Circular glass disk recessed from stone ring. geo_prism(o,[(rr*cos(k*pi/32),zc+rr*sin(k*pi/32)) for k in range(64)],.14,.22,'glass') if level: for x in [-.8,-.4,0,.4,.8]: dz=math.sqrt(rr*rr-x*x);geo_box(o,(x,-.08,zc),(.045,.09,2*dz),'stone') for z in [-.7,0,.7]: dx=math.sqrt(rr*rr-z*z);geo_box(o,(0,-.08,zc+z),(2*dx,.09,.045)) # Wreath leaves and central lyre support the characteristic circular grille. for j in range(32): a=j*2*pi/32;u,v=1.73*cos(a),zc+1.73*sin(a) geo_beam(o,(u-.14*sin(a),-.22,v+.14*cos(a)),(u+.14*sin(a),-.22,v-.14*cos(a)),.12,.06) if level>=2: for s in [-1,1]:geo_tube(o,[(s*.42,-.27,zc+.8),(s*.32,-.3,zc+.1),(s*.65,-.27,zc-.7),(s*.35,-.28,zc-1)],.055) geo_box(o,(0,-.27,zc+.72),(.9,.12,.09));geo_box(o,(0,-.27,zc-.93),(.9,.12,.12)) for x in [-.22,0,.22]:geo_box(o,(x,-.26,zc),(.025,.08,1.5)) for x in [-2.36,2.36]: geo_box(o,(x,-.17,21.8),(.22,.3,5.4));geo_box(o,(x,-.2,24.45),(.52,.38,.25)) proto=geo_finish(o,'Oculus01','crown',(0,-15.88,0)) for j in range(1,20): a=j*2*pi/20;instance(proto,f'Oculus{j+1:02}','crown',(15.88*sin(a),-15.88*cos(a),0),a) geo_lathe(g,(0,0,0),DOME,'roof_metal',200) for j in range(20): a=-pi/2+(j+.5)*pi/10 geo_tube(g,[(r*cos(a),r*sin(a),z+.04) for r,z in DOME],.12 if level else .07,'roof_metal',8) if level: for off in [-.052,.052]:geo_tube(g,[(r*cos(a+off),r*sin(a+off),z+.015) for r,z in DOME],.018,'roof_metal',6) if level>=2: # Raised scroll/rib ends at eaves, derived from silhouette, not exact sculpture. for off in [-.23,.23]: p=[] for k in range(40): t=k/39*2*pi;rad=.62*(1-k/47);tr=16.1+rad*cos(t);z=27.1+rad*sin(t);tang=off p.append((tr*cos(a)-tang*sin(a),tr*sin(a)+tang*cos(a),z)) geo_tube(g,p,.065,'zinc') if level>=3: x,y=16.35*cos(a),16.35*sin(a) geo_beam(g,(x,y,26.3),(x,y,27.7),.24,.35,'zinc') # The upper roof is a sheet-covered shallow cap, not an open cage. geo_lathe(g,(0,0,0),[(5.35,36.25),(5.55,36.48),(5.4,36.65),(4.6,37.05),(3.3,37.6),(1.25,38.13),(.62,38.28)],'roof_metal',128) for rr,z,h in [(5.4,36.25,.18),(5.55,36.48,.16),(5.42,36.65,.1),(1.35,38.12,.15)]:geo_lathe(g,(0,0,z),[(rr,0),(rr,h),(rr-.14,h),(rr-.14,0),(rr,0)],'roof_metal',128) geo_lathe(g,(0,0,38.35),[(.55,0),(.7,.2),(.6,.7),(.32,.9),(.34,1.25),(.12,1.4)],'zinc',64) geo_lathe(g,(0,0,39.7),[(.055,0),(.055,1.3)],'roof_metal',16) if level: for j in range(180): a=j*pi/90;geo_box(g,(16.43*cos(a),16.43*sin(a),25.57),(.12,.12,.23)) ob=geo_finish(g,'Drum cornices ribbed zinc dome crown finial','crown') roof_uv(ob) def setup_cameras(): s=scene();c=collection('presentation') if not c.objects: cameras={'hero':((66,-96,64),(0,2,17),76),'front':((0,-115,20),(0,0,20),58),'rear':((-62,96,58),(0,6,15),78),'side':((110,4,24),(0,4,15),70),'top':((0,3,130),(0,3,0),74),'portico':((30,-58,24),(0,-21,9),38),'facade':((56,-23,21),(23,0,9),34),'dome':((40,-56,43),(0,0,28),43),'court':((-41,65,63),(0,22,10),48)} for name,(loc,target,scale) in cameras.items(): ca=bpy.data.cameras.new(PREFIX+name);ob=bpy.data.objects.new(PREFIX+name,ca);c.objects.link(ob);ob.location=loc;ob.rotation_euler=(Vector(target)-Vector(loc)).to_track_quat('-Z','Y').to_euler();ca.type='ORTHO';ca.ortho_scale=scale;ca.clip_end=500 li=bpy.data.lights.new(PREFIX+'Sun','SUN');li.energy=3;li.angle=.15;ob=bpy.data.objects.new(PREFIX+'Sun',li);c.objects.link(ob);ob.rotation_euler=(.55,-.6,-.45) li=bpy.data.lights.new(PREFIX+'Fill','AREA');li.energy=1600;li.shape='DISK';li.size=30;ob=bpy.data.objects.new(PREFIX+'Fill',li);c.objects.link(ob);ob.location=(35,-45,50);ob.rotation_euler=(Vector((0,0,15))-ob.location).to_track_quat('-Z','Y').to_euler() s.camera=bpy.data.objects[PREFIX+'hero'] def build_section(sec,level): assert sec in BASE_SECTIONS and 0<=level<=3 scene();clear_section(sec) if sec=='shell':shell(level) elif sec=='portico':portico(level) elif sec=='elevations':elevations(level) elif sec=='roofs':roofs(level) elif sec=='crown':crown(level) scene()['level_'+sec]=level bpy.context.view_layer.update() print(json.dumps({'section':sec,'level':level,'objects':len(collection(sec).objects)})) def build(levels=None): scene();levels=levels or {s:3 for s in BASE_SECTIONS} for sec in BASE_SECTIONS:build_section(sec,levels[sec]) for sec in REAR_SECTIONS:build_rear_section(sec,PROGRESS[sec]) setup_cameras();return scene() def save(path): scene();bpy.ops.wm.save_as_mainfile(filepath=str(path),copy=True,compress=True);print('SAVED COPY',path) # Rear coordinates follow the existing skewed street line. All dimensions are photo estimates. REAR_ANGLE=-math.atan(.19) def rear_point(u,v,z): return (u,17.8-.19*u+v,z) def rear_finish(g,name,sec): for i,(u,v,z) in enumerate(g['v']):g['v'][i]=rear_point(u,v,z) ob=geo_finish(g,name,sec);roof_uv(ob);return ob def roof_uv(ob): # Horizontal metre projection avoids the radial pinching of dominant-normal UVs. if ob.type!='MESH':return layer=ob.data.uv_layers.active for p in ob.data.polygons: if ob.data.materials[p.material_index].name!=PREFIX+'roof_metal':continue for li in p.loop_indices: v=ob.data.vertices[ob.data.loops[li].vertex_index].co layer.data[li].uv=(v.x/2,v.y/2 if abs(p.normal.z)>.3 else v.z/2) def rear_body(level): g=Geo() # The roofed rear hall replaces the previously inferred courtyard; small flank slots remain. outline=[(14.5*cos(j*pi/96),10.45*sin(j*pi/96)) for j in range(97)] slabpoly(g,outline,.65,16.1,'rear_plaster') # A low zinc-clad curved drum supports the semicircular roof above the perimeter roofs. for j in range(96): a,b=j*pi/96,(j+1)*pi/96 p0=(14.5*cos(a),10.45*sin(a));p1=(14.5*cos(b),10.45*sin(b)) geo_add(g,[(p0[0],p0[1],16.1),(p1[0],p1[1],16.1),(p1[0],p1[1],20.25),(p0[0],p0[1],20.25)],[(0,1,2,3)],'roof_metal') # Straight inner wall follows the pitch of the roof instead of closing the dome-side slot. geo_prism(g,[(-14.5,18.4),(14.5,18.4),(14.5,20.25),(0,23.5),(-14.5,20.25)],-.14,.15,'roof_metal') if level>=1: for side in [-1,1]: # Hipped end masses flank the semicircle and connect it to the rear street wing. outline=[(side*14.5,1),(side*21.8,1),(side*21.8,10.7),(side*14.5,10.7)] slabpoly(g,outline if side==1 else outline[::-1],.65,15.6,'rear_plaster') geo_box(g,(0,10.2,8.1),(44.5,3.4,14.9),'rear_plaster') # Central raised connector and small circular crest at the roof chord. if level==1: geo_box(g,(0,-1.4,22.65),(1.5,2.8,.55),'stone') geo_box(g,(0,-1.4,23.08),(1.85,2.9,.18),'stone') else: geo_beam(g,(0,-2.7,25.15),(0,-.1,23.55),1.5,.5,'stone') for x in [-.78,.78]:geo_beam(g,(x,-2.7,25.47),(x,-.1,23.87),.16,.23,'stone') geo_ring(g,(0,-.23,23.2),.78,.1,'roof_metal') geo_prism(g,[(.7*cos(j*pi/32),23.2+.7*sin(j*pi/32)) for j in range(64)],-.17,-.08,'roof_metal') if level>=2: for v in [-.32,.28]: geo_beam(g,(-14.5,v,20.32),(0,v,23.62),.16,.19,'stone');geo_beam(g,(0,v,23.62),(14.5,v,20.32),.16,.19,'stone') for side in [-1,1]: geo_box(g,(side*14.6,0,19.55),(.38,.65,2.05),'stone') geo_box(g,(side*14.6,0,20.6),(.65,.9,.22),'stone') for j in range(12): a=j*pi/6;geo_beam(g,(.12*cos(a),-.28,23.2+.12*sin(a)),(.65*cos(a),-.28,23.2+.65*sin(a)),.025,.035,'roof_metal') rear_finish(g,'Rear hall curved skirt chord and connector','rear_body') def semi_point(t,r):return (14.55*r*cos(t),10.5*r*sin(t),20.3+3.25*(1-r)) def rear_roof(level): g=Geo();n=96 # Fan triangles have one apex, avoiding collapsed rings at the centre. apex=(0,0,23.55) for j in range(n): a,b=j*pi/n,(j+1)*pi/n geo_add(g,[apex,semi_point(a,1),semi_point(b,1)],[(0,1,2)],'roof_metal',True) if level>=1: for j in range(1,48): t=j*pi/48;geo_tube(g,[tuple(v+(0.025 if k==2 else 0) for k,v in enumerate(semi_point(t,r))) for r in [.025,.2,.4,.6,.8,1]],.025,'roof_metal',6) geo_tube(g,[semi_point(j*pi/128,1) for j in range(129)],.09,'roof_metal',8) if level>=2: # Concentric sheet courses are fine lap joints; they must not read as masonry blocks. for r in [.15,.28,.41,.54,.67,.8,.93]: geo_tube(g,[(u,v,z+.018) for u,v,z in [semi_point(j*pi/128,r) for j in range(129)]],.014,'roof_metal',5) for j in range(80): t=(j+.5)*pi/80 geo_beam(g,(14.52*cos(t),10.47*sin(t),20.22),(14.52*cos(t),10.47*sin(t),19.1),.022,.035,'roof_metal') rear_finish(g,'Semicircular rear metal roof','rear_roof') def horizontal_ring(g,x,y,z,r,th=.035,m='equipment'): geo_tube(g,[(x+r*cos(j*pi/32),y+r*sin(j*pi/32),z) for j in range(64)],th,m,6,True) def roof_equipment(level): g=Geo() # Lower metal deck closes the exterior wedges around the curved hall; the dome-side slot stays open. slabpoly(g,[(-14.75,0),(14.75,0),(14.75,12.65),(-14.75,12.65)],16.15,16.4,'roof_metal') # Rear perimeter roof has a narrow flat service deck, rather than a broad pitched strip. slabpoly(g,[(-23.6,10.1),(23.6,10.1),(23.6,12.65),(-23.6,12.65)],16.15,16.4,'roof_metal') # Wider west service roof is hipped around a broad equipment plateau. v=[(-23.6,-5,16.3),(-14.75,-5,16.3),(-14.75,12.5,16.3),(-23.6,12.5,16.3),(-22.4,5.3,18.1),(-15.1,5.3,18.1),(-15.1,10.7,18.1),(-22.4,10.7,18.1)] geo_add(g,v,[(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7),(4,5,6,7)],'roof_metal') for a,b in [(0,4),(1,5),(2,6),(3,7),(4,5),(5,6),(6,7),(7,4)]:geo_tube(g,[v[a],v[b]],.04,'roof_metal') before=len(g['m']);hip(g,14.8,23.5,-1,12.5,16.3,2.25,max(1,level)) for i in range(before,len(g['m'])): if g['m'][i]=='zinc':g['m'][i]='roof_metal' equipment_start=len(g['v']) # Two visible fan arrays sit together on the wider left rear roof in the supplied view. for x in [-20.6,-16.4]: y=8.15 if level>=1: geo_box(g,(x,y,17.45),(3.25,4.65,2.5),'roof_metal') geo_box(g,(x,y,18.8),(3.25,4.65,.24),'grille') for dy in [-1.75,1.75]:geo_box(g,(x,y+dy,18.65),(3.5,.28,.32),'equipment') geo_box(g,(x,y,19.65),(2.9,4.3,1.45),'equipment') geo_box(g,(x,y,20.41),(3.05,4.45,.1),'equipment') for dx in [-.7,.7]: for dy in ([-1.55,-.52,.52,1.55] if level>=2 else [-1.4,0,1.4]): horizontal_ring(g,x+dx,y+dy,20.49,.46 if level>=2 else .57,.045) # Concentric dark rings form the recessed fan face. for rad in ([.1,.2,.3,.4] if level>=2 else [.12,.25,.38,.49]):horizontal_ring(g,x+dx,y+dy,20.46,rad,.045,'grille') if level>=2: for j in range(8): a=j*pi/4;geo_beam(g,(x+dx+.1*cos(a),y+dy+.1*sin(a),20.51),(x+dx+.42*cos(a+.2),y+dy+.42*sin(a+.2),20.51),.035,.018,'equipment') if level>=2: for z in [19.05,19.35,19.65,19.95]: for side in [-1,1]:geo_box(g,(x+side*1.47,y,z),(.035,3.9,.14),'grille') for yy in [-2.16,2.16]:geo_box(g,(x,y+yy,z),(2.5,.025,.14),'grille') for yy in [-2.2,2.2]:geo_beam(g,(x-1.3,y+yy,19.03),(x+1.3,y+yy,20.19),.07,.05,'equipment') # Square raised vent/chimney housing, adjacent to the arrays. if level>=1:geo_box(g,(-20.6,3.65,17.45),(2.15,2.05,2.5),'roof_metal') geo_box(g,(-20.6,3.65,19.25),(2.15,2.05,1.55),'stone') geo_box(g,(-20.6,3.65,20.1),(2.4,2.3,.18),'stone') geo_box(g,(-20.9,3.65,20.21),(1,1.35,.05),'grille') if level>=2:geo_box(g,(-20.0,3.65,20.21),(.4,.75,.05),'grille') for i in range(equipment_start,len(g['v'])): u,v,z=g['v'][i];g['v'][i]=(u,v,z-.7) if level>=2: for j in range(12): u=-22.25+j*.63 geo_tube(g,[(u,-5,16.32),(u,5.3,18.12),(u,10.7,18.12),(u,12.5,16.32)],.018,'roof_metal',5) for j in range(39): u=-14.4+j*.74;geo_tube(g,[(u,0,16.42),(u,12.65,16.42)],.014,'roof_metal',5) geo_tube(g,[(-23.65,12.67,16.46),(23.65,12.67,16.46)],.07,'roof_metal') for side in [-1,1]: geo_tube(g,[(side*23.65,v,16.45) for v in [0,10,12.65]],.075,'roof_metal') for x in [-23.65,23.65]: geo_box(g,(x,12.25,16.68),(.32,.8,.55),'roof_metal') rear_finish(g,'Rear hips service deck twin fan arrays and vent','roof_equipment') def rear_facade(level): # Seven bays follow the confirmed street photograph: doors, blind masonry and shutters. kinds=['blind','door','blind','blind','door','glass','blind'] for j,kind in enumerate(kinds): g=bay(3,blind=kind!='glass' or level>=2) if kind=='glass' and level>=2: for x in [-1.68,0,1.68]:geo_box(g,(x,.11,6.1),(1.38,.04,5.25),'glass') geo_box(g,(0,-.14,11.45),(1.7,.16,2.65),'glass') for x in [-.94,0,.94]:geo_box(g,(x,-.24,11.45),(.12,.25,2.85),'stone') # Painted blind panels have stone frames; there are no timber-framed pretend windows. if kind!='glass':g['m']=['stone' if m=='wood' else m for m in g['m']] if kind=='door': geo_box(g,(0,-.24,4.1),(1.9,.18,5.45),'wood') for x in [-1.16,1.16]:geo_box(g,(x,-.3,4.15),(.28,.45,5.9),'stone') geo_box(g,(0,-.33,7.2),(2.85,.6,.3),'stone') geo_box(g,(0,-.42,7.53),(2.45,.5,.22),'stone') if level>=1: for x in [-.48,.48]: for z in [2.35,3.7,5.05,6.1]: geo_box(g,(x,-.355,z),(.72,.055,.84),'dark') for dx in [-.38,.38]:geo_box(g,(x+dx,-.39,z),(.06,.08,.94),'wood') geo_ring(g,(0,-.45,7.8),.28,.07,'stone') if level>=2: for side in [-1,1]: geo_tube(g,[(side*.2,-.4,7.65),(side*.65,-.4,7.98),(side*1,-.4,7.85),(side*1.2,-.4,7.65)],.055,'stone') # Upper lunettes are predominantly blind with rectangular paired shutters/windows. if kind!='glass': shutter=j in [0,1,4,6] if shutter: geo_box(g,(0,-.14,11.45),(1.7,.16,2.65),'grille') for x in [-.94,0,.94]:geo_box(g,(x,-.24,11.45),(.12,.25,2.85),'stone' if x else 'wood') for z in [10.04,12.9]:geo_box(g,(0,-.25,z),(2.05,.3,.14),'stone') if level>=1: for z in [10.2+k*.12 for k in range(22)]:geo_box(g,(0,-.25,z),(1.65,.12,.035),'wood') if level>=2: # Fine runoff is placed under the horizontal ledge, not repeated in the plaster tile. for x in [-2.5,2.5]:geo_box(g,(x,-.085,8.82),(.045,.02,.35),'rear_plaster') ob=geo_finish(g,f'Rear street bay{j+1:02} {kind}','rear_facade') ob.location=rear_point(-19.3+j*6.43,12.25,0);ob.rotation_euler.z=pi+REAR_ANGLE # Rusticated quoins close the skewed rear corners. g=Geo() for x in [-23.65,23.65]: geo_box(g,(x,12.18,8),(.65,1.2,15.1),'plaster') if level>=1: for z in [1.2+k*.45 for k in range(32)]:geo_box(g,(x,12.83,z),(.8,.09,.055),'stone') rear_finish(g,'Rear corner quoins','rear_facade') def dome_roof(level): g=Geo() if level>=1: # Fine standing seams between the twenty principal decorative ribs. for j in range(100): a=-pi/2+j*2*pi/100 geo_tube(g,[(r*cos(a),r*sin(a),z+.025) for r,z in DOME],.019,'roof_metal',5) for j in range(32): a=j*pi/16 geo_tube(g,[(r*cos(a),r*sin(a),z+.025) for r,z in [(5.42,36.67),(4.6,37.05),(3.3,37.6),(1.25,38.13)]],.025,'roof_metal',6) if level>=2: for j in range(20): a=-pi/2+(j+.5)*pi/10 # Broad raised rectangular rib ends are clearly visible in all three aerials. u=Vector((cos(a),sin(a),0));t=Vector((-sin(a),cos(a),0)) lo=u*15.95+Vector((0,0,27.65));hi=u*14.9+Vector((0,0,29.55)) pts=[tuple(lo-t*.23),tuple(lo+t*.23),tuple(hi+t*.23),tuple(hi-t*.23)] geo_add(g,pts+[(x,y,z-.16) for x,y,z in pts],[(0,1,2,3),(7,6,5,4),(0,4,5,1),(1,5,6,2),(2,6,7,3),(3,7,4,0)],'roof_metal') for sign in [-1,1]:geo_beam(g,tuple(lo+t*.23*sign),tuple(hi+t*.23*sign),.06,.08,'roof_metal') geo_lathe(g,(0,0,0),[(.62,38.23),(.55,38.4)],'roof_metal',48) for j in [2,10,18,26]: a=j*pi/16;x,y=3.45*cos(a),3.45*sin(a) geo_box(g,(x,y,37.57),(.58,.72,.12),'roof_metal') ob=geo_finish(g,'Dome standing seams rib plates and cap hatches','dome_roof');roof_uv(ob) def build_rear_section(sec,level): assert sec in REAR_SECTIONS and 0<=level<=2 clear_section(sec) if sec=='rear_body':rear_body(level) elif sec=='rear_roof':rear_roof(level) elif sec=='roof_equipment':roof_equipment(level) elif sec=='rear_facade':rear_facade(level) elif sec=='dome_roof':dome_roof(level) scene()['rear_'+sec]=level bpy.context.view_layer.update() def configure(progress=None): if progress is not None: assert set(progress)==set(REAR_SECTIONS) and all(type(v)==int and 0<=v<=2 for v in progress.values()) PROGRESS.update(progress) return build()