Add a garden Buddha and meditation tour chapter

This commit is contained in:
2026-09-10 21:41:51 +10:00
parent 79c5eeddbb
commit 0582fc44df
14 changed files with 5869 additions and 12 deletions

View File

@@ -0,0 +1,154 @@
import bpy,math,os,json
from mathutils import Vector,Matrix
from math import sin,cos,pi
O=os.path.dirname(__file__);s=bpy.context.scene
c=bpy.data.collections.new('36 Buddha - meditation garden');bpy.data.collections['DETAIL - full geometry for renders'].children.link(c);COL={'new':c};active='new'
src=open(O+'/../samford-modular/build_scene.py').read();exec(src[src.index('def mesh('):src.index('timber=textured(')])
cubefs=[tuple(reversed(f)) for f in cubefs]
stone=textured('Buddha - warm weathered limestone',(.31,.285,.235),(.49,.46,.38),(42,42,42),.83,.0018)
shade=mat('Buddha - fine carved recess',(.18,.16,.125),.94)
def ell(n,p,sz,ma=stone,rot=0,nu=40,nv=24):
vs=[];fs=[]
for j in range(nv+1):
a=pi*j/nv
for i in range(nu):
b=2*pi*i/nu;x=sz[0]*sin(a)*cos(b);y=sz[1]*sin(a)*sin(b)
vs.append((p[0]+x*cos(rot)-y*sin(rot),p[1]+x*sin(rot)+y*cos(rot),p[2]+sz[2]*cos(a)))
for j in range(nv):
for i in range(nu):k=j*nu+i;l=j*nu+(i+1)%nu;fs.append((k,l,l+nu,k+nu))
ob=mesh('BUDDHA - '+n,vs,[tuple(reversed(f)) for f in fs],ma)
for f in ob.data.polygons:f.use_smooth=True
return ob
def tube(n,points,r,ma=stone):
# Catmull-Rom smooth carved lines / rounded limbs.
pts=[];p=[Vector(q) for q in points]
for i in range(len(p)-1):
a=p[max(0,i-1)];b=p[i];cc=p[i+1];d=p[min(i+2,len(p)-1)]
for j in range(8):
t=j/8;pts.append(.5*(2*b+(-a+cc)*t+(2*a-5*b+4*cc-d)*t*t+(-a+3*b-3*cc+d)*t*t*t))
pts.append(p[-1]);vs=[];fs=[]
for i,v in enumerate(pts):
q=(pts[min(i+1,len(pts)-1)]-pts[max(0,i-1)]).to_track_quat('Z','Y')
rr=r*(.65+.35*sin(pi*i/(len(pts)-1)))
for j in range(10):vs.append(tuple(v+q@Vector((rr*cos(j*2*pi/10),rr*sin(j*2*pi/10),0))))
for i in range(len(pts)-1):
for j in range(10):k=i*10+j;l=i*10+(j+1)%10;fs.append((k,l,l+10,k+10))
fs.extend([tuple(reversed(range(10))),tuple(range(len(vs)-10,len(vs)))])
ob=mesh('BUDDHA - '+n,vs,fs,ma)
for f in ob.data.polygons:f.use_smooth=True
return ob
box('BUDDHA - low stone plinth',(0,0,.13),(1.5,1.12,.26),stone,.04)
box('BUDDHA - plinth cap',(0,0,.285),(1.56,1.18,.075),stone,.025)
ell('oval seat',(0,0,.37),(.65,.43,.075))
ell('left folded robe knee',(-.30,-.075,.52),(.39,.32,.16),rot=-.22)
ell('right folded robe knee',(.30,-.075,.52),(.39,.32,.16),rot=.22)
ell('crossed shin left',(-.13,-.25,.555),(.40,.145,.13),rot=.23)
ell('crossed shin right',(.15,-.22,.585),(.39,.14,.12),rot=-.24)
ell('left resting foot',(.30,-.25,.655),(.19,.09,.075),rot=.25)
ell('right resting foot',(-.29,-.28,.64),(.18,.09,.072),rot=-.20)
# Sculpted torso profile narrows at waist and neck, rounded shoulders.
vs=[];fs=[];profiles=[(.53,.26,.18),(.68,.235,.185),(.86,.24,.175),(1.05,.30,.16),(1.16,.30,.14),(1.23,.20,.12),(1.25,.105,.095)]
for z,rx,ry in profiles:
for i in range(48):a=i*2*pi/48;vs.append((rx*cos(a),.02+ry*sin(a),z))
for j in range(len(profiles)-1):
for i in range(48):k=j*48+i;l=j*48+(i+1)%48;fs.append((k,l,l+48,k+48))
fs.extend([tuple(reversed(range(48))),tuple(range(len(vs)-48,len(vs)))])
ob=mesh('BUDDHA - draped torso',vs,fs,stone)
for f in ob.data.polygons:f.use_smooth=True
mod=ob.modifiers.new('Soft carved robe','SUBSURF');mod.levels=2;mod.render_levels=2
ell('neck',(0,.015,1.265),(.10,.10,.13))
for sign in [-1,1]:
ell('rounded robe shoulder',(sign*.265,0,1.135),(.095,.091,.107))
tube('relaxed arm',[(sign*.265,0,1.15),(sign*.32,-.015,.99),(sign*.34,-.08,.80),(sign*.22,-.255,.725),(sign*.085,-.30,.715)],.075)
ell('hand in meditation',(sign*.052,-.325,.73+sign*.012),(.13,.073,.037),rot=sign*.08)
for i in range(3):tube('finger crease',[(sign*.015,-.375-i*.008,.734+sign*.012),(sign*.11,-.365-i*.008,.728+sign*.012)],.0014,shade)
tube('touching thumbs',[(-.09,-.30,.755),(-.035,-.29,.79),(0,-.30,.785),(.035,-.29,.79),(.09,-.30,.755)],.014)
# A serene face, lowered eyelids, elongated earlobes and ushnisha.
ell('head',(0,0,1.48),(.157,.136,.21))
ell('chin',(0,-.033,1.355),(.100,.088,.067))
for sign in [-1,1]:
ell('ear',(sign*.157,.002,1.46),(.037,.050,.104))
ell('elongated earlobe',(sign*.158,-.002,1.365),(.026,.028,.055))
tube('ear carving',[(sign*.168,-.036,1.52),(sign*.181,-.042,1.47),(sign*.171,-.037,1.40)],.004,shade)
tube('gentle brow',[(sign*.022,-.127,1.527),(sign*.068,-.127,1.545),(sign*.113,-.105,1.53)],.009)
tube('lowered eyelid',[(sign*.026,-.131,1.497),(sign*.061,-.133,1.486),(sign*.102,-.112,1.493)],.0035,shade)
ell('nose bridge',(0,-.133,1.46),(.024,.029,.064))
ell('nose tip',(0,-.155,1.423),(.029,.024,.017))
tube('calm closed mouth',[(-.047,-.116,1.387),(-.022,-.130,1.392),(0,-.133,1.389),(.022,-.130,1.392),(.047,-.116,1.387)],.0028,shade)
ell('lower lip',(0,-.123,1.378),(.037,.010,.009))
ell('ushnisha topknot',(0,.022,1.723),(.087,.086,.092))
for j in range(7):
a=.16+j*.14;num=max(9,int(36*sin(a)))
for i in range(num):
b=2*pi*(i+.5*(j%2))/num
ell('carved hair curl',(.161*sin(a)*cos(b),.143*sin(a)*sin(b),1.48+.214*cos(a)),(.018,.018,.015),nu=10,nv=6)
for j in range(6):
a=.20+j*.29;num=max(6,int(24*sin(a)))
for i in range(num):
b=2*pi*(i+.5*(j%2))/num
ell('topknot carving',(.087*sin(a)*cos(b),.022+.086*sin(a)*sin(b),1.723+.092*cos(a)),(.013,.013,.011),nu=8,nv=6)
# Robe seam and restrained shallow folds.
tube('robe diagonal edge',[(-.18,-.095,1.20),(-.11,-.16,1.10),(.02,-.174,.97),(.15,-.15,.86),(.20,-.13,.77)],.013)
for j in range(5):
z=.61+j*.075;tube('robe fold',[(-.18,-.13,z+.04),(-.05,-.173,z),(.11,-.155,z-.008)],.0045)
for sign in [-1,1]:
for j in range(3):tube('knee robe fold',[(sign*.22,-.37,.57+j*.022),(sign*.42,-.30,.57+j*.025),(sign*.55,-.17,.55+j*.027)],.005)
# Fuse the neck, shoulders and arms into one continuous stone carving.
detail=bpy.data.collections['DETAIL - full geometry for renders'];was_hidden=detail.hide_viewport;detail.hide_viewport=False;bpy.context.view_layer.update()
parts=[ob for ob in c.objects if any(k in ob.name for k in ['draped torso','rounded robe shoulder','relaxed arm',' - neck'])]
vs=[];fs=[];dg=bpy.context.evaluated_depsgraph_get()
for ob in parts:
ev=ob.evaluated_get(dg);me=ev.to_mesh();off=len(vs);vs.extend(tuple(v.co) for v in me.vertices);fs.extend(tuple(off+i for i in p.vertices) for p in me.polygons);ev.to_mesh_clear()
body=mesh('BUDDHA - continuous carved upper body',vs,fs,stone)
bpy.context.view_layer.objects.active=body
rem=body.modifiers.new('Joined limestone carving','REMESH');rem.mode='VOXEL';rem.voxel_size=.007;rem.use_smooth_shade=True
bpy.ops.object.modifier_apply(modifier=rem.name)
sm=body.modifiers.new('Stone carving finish','SMOOTH');sm.factor=.65;sm.iterations=5;bpy.ops.object.modifier_apply(modifier=sm.name)
for ob in parts:bpy.data.objects.remove(ob,do_unlink=True)
detail.hide_viewport=was_hidden
# Rotate east to look toward shared court; north of meditation room, south of garden path.
cx,cy=-12.4,15.35
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
zbase=max(ground(cx+x,cy+y) for x in [-.6,.6] for y in [-.8,.8])+.025
T=Matrix.Translation((cx,cy,zbase))@Matrix.Rotation(pi/2,4,'Z')
for ob in c.objects:ob.matrix_world=T@Matrix.LocRotScale(ob.location,ob.rotation_euler.to_quaternion(),ob.scale)
# Single smooth lightweight viewport proxy, no object overhead from small carving details.
vs=[];fs=[];inds=[];smooth=[];mats=[]
for ob in c.objects:
mw=Matrix.LocRotScale(ob.location,ob.rotation_euler.to_quaternion(),ob.scale);off=len(vs);vs.extend(tuple(mw@v.co) for v in ob.data.vertices)
for m in ob.data.materials:
if m not in mats:mats.append(m)
for p in ob.data.polygons:fs.append(tuple(off+i for i in p.vertices));inds.append(mats.index(ob.data.materials[p.material_index]));smooth.append(p.use_smooth)
me=bpy.data.meshes.new('Buddha garden viewport');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',smooth)
proxy=bpy.data.objects.new('FAST | Garden Buddha statue',me);bpy.data.collections['00 FAST TOUR - lightweight viewport'].objects.link(proxy)
chapters=json.load(open(O+'/tour-chapters-container-water.json'));title='31 / QUIET GARDEN BUDDHA';chapters.append(dict(title=title,start=3601,end=3720,seconds=5))
cam=bpy.data.objects['PLAY TOUR camera'];s.frame_end=3720
for f,p in [(3601,(-7.5,18.2,2.6)),(3720,(-7.8,17.7,2.35))]:
s.frame_set(f);cam.location=p;cam.rotation_euler=(Vector((cx,cy,zbase+.95))-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=49;cam.data.keyframe_insert('lens',frame=f)
coll=bpy.data.collections['22 PLAY - guided camera tour'];fonts=[o for o in coll.objects if o.type=='FONT']
ref=next(o for o in fonts if o.data.body.startswith('30 /'))
ref.scale=(0,0,0);ref.keyframe_insert('scale',frame=3601)
caption=ref.copy();caption.data=ref.data.copy();caption.animation_data_clear();coll.objects.link(caption);caption.name='Tour caption 31 - garden Buddha';caption.data.body=title;caption.location=(-36/49*.91,36/49*9/16*.84,-2)
for f,k in [(1,0),(3600,0),(3601,1),(3720,1)]:caption.scale=(k,k,k);caption.keyframe_insert('scale',frame=f)
for ob in [ref,caption]:
for layer in ob.animation_data.action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for fc in bag.fcurves:
for key in fc.keyframe_points:key.interpolation='CONSTANT'
s.timeline_markers.new(title,frame=3601);s['PLAY THE TOUR']='Hover over the 3D view and press Space. 31 chapters, 2 min 35 sec. Shift+Left Arrow restarts.'
s['Garden Buddha']='Weathered limestone seated Buddha on a low stone plinth, north of the meditation pavilion, facing east into the shared garden court. Version 006.'
text=bpy.data.texts.get('START HERE - PRESS PLAY')
if text:text.clear();text.write(s['PLAY THE TOUR']+'\n\n'+s['Garden Buddha'])
layout=json.load(open(O+'/container-water-layout.json'));layout.update(version=6,buddha=[cx,cy,zbase],buddha_facing='east')
json.dump(layout,open(O+'/buddha-garden-layout.json','w'),indent=2);json.dump(chapters,open(O+'/tour-chapters-buddha-garden.json','w'),indent=2)
s.frame_set(3601);s.render.resolution_percentage=100;s.camera=cam
bpy.ops.wm.save_as_mainfile(filepath=O+'/samford-solarpunk-buddha-garden-tour.blend')
for o in fonts+[caption]:o.hide_render=True
s.render.resolution_percentage=60;s.cycles.samples=48;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=O+'/buddha-garden.png';bpy.ops.render.render(write_still=True)
print('BUDDHA_READY',len(c.objects),len(fs),flush=True)

View File

@@ -0,0 +1,19 @@
import bpy,json,os,math
from mathutils import Vector
O=os.path.dirname(__file__);s=bpy.context.scene;chapters=json.load(open(O+'/tour-chapters-buddha-garden.json'));fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT'];issues=[]
for ch in chapters:
s.frame_set(ch['start']+30);shown=[o.data.body for o in fonts if o.scale.length>.5]
if shown!=[ch['title']]:issues.append([ch['title'],shown])
names=[o.name for o in s.objects];layout=json.load(open(O+'/buddha-garden-layout.json'));x,y,z=layout['buddha']
# Actual rotated rectangular plinth lies outside the pavilion and walking-path ribbon.
mx,my,mw,md=layout['meditation'];assert y-.78>my+md/2
clearance=999
for path in layout['paths']:
if path['kind']=='roofed walk':continue
for q in path['points']:
dist=math.hypot(max(abs(q[0]-x)-.59,0),max(abs(q[1]-y)-.78,0))-path['width']/2
clearance=min(clearance,dist)
assert clearance>.05,clearance
report={'buddha_collection_objects':len(bpy.data.collections['36 Buddha - meditation garden'].objects),'viewport_proxy':'FAST | Garden Buddha statue' in names,'tour_chapters':len(chapters),'caption_issues':issues,'path_clearance_m':round(clearance,3),'statue_outside_room':True,'internal_water_tanks':sum(n.startswith('INTERNAL RAINWATER TANK') for n in names),'kitchen_preserved':'ISLAND - honed marble countertop' in names,'no_table_tennis':not any('table tennis' in n.lower() for n in names),'render_size':[s.render.resolution_x,s.render.resolution_y,s.render.resolution_percentage]}
assert len(chapters)==31 and s.frame_end==3720 and not issues and report['viewport_proxy'] and report['internal_water_tanks']==2 and report['kitchen_preserved'] and report['no_table_tennis'] and report['render_size']==[2560,1440,100]
json.dump(report,open(O+'/buddha-garden-verification.json','w'),indent=2);print('VERIFIED '+json.dumps(report),flush=True)