Add enhanced house presentation, client film and walkable Unreal prototype

This commit is contained in:
2026-09-12 23:28:13 +10:00
parent 0582fc44df
commit 544ff143f8
516 changed files with 5318 additions and 15 deletions

View File

@@ -0,0 +1,15 @@
# Samford presentation and first walkable homestead — v007
Goal: improve the existing approved Blender concept and provide an actual shareable video plus an editable first-person Unreal prototype of the whole site. Preserve all v006 layout decisions and historical files.
Design: natural material detail at human scale; finished edges, soft mineral surfaces, timber end grain and grain variation, fuller garden planting and visible lighting details. Retain the open presentation sides and future ghost pavilions. No new architectural programme or claim of surveyed/construction accuracy.
Implementation:
- [x] Inspect the v006 saved scene and improve materials, architectural detail and planting in a new v007 file. Keep the 31-chapter inspection tour and FAST/DETAIL split.
- [x] Render a small set of actual Cycles views, inspect them, and build a separate short cinematic with deliberate camera moves. Encode an H.264 MP4 for ordinary client playback.
- [x] Export evaluated full-site meshes in logical groups, with transferable material maps. Import into a separate Unreal 5.8 project, preserve the existing meditation study.
- [x] Configure the bundled ArchVisCharacter with WASD/mouse, real floor/terrain collision and furniture/wall obstacles. Save an opening level and presentation cameras.
- [x] Test the saved Unreal level, floor traces and actual first-person movement; capture a real UE preview. Label the result a prototype rather than a packaged production simulation.
- [ ] Save model, video, Unreal assets and usage/context documentation to the public server project; verify repository and published gallery.
Validation: saved-file object/material checks; unchanged building coordinates and preserved features; view actual rendered images and video contact sheet; inspect video dimensions/duration; verify Unreal assets, collision and pawn movement. The MP4 is a concept presentation, not engineering approval or an exact representation of a surveyed property.

View File

@@ -0,0 +1,39 @@
# Samford Valley — presentation and walkable prototype, v007
This revision develops the approved Samford Valley concept for design conversations and client presentations. It preserves the modular layout, future shadow pavilions, natural material direction and all previous house features.
## Watch the film
`video/Samford-Valley-Concept-Film-v007.mp4` is the client presentation master. It contains real animated Blender camera views, introductory and closing cards, and short captions. H.264 MP4, 1920 × 1080, 24 fps, 30 seconds, no audio. It plays in QuickTime or a normal browser.
The film uses Cycles source frames rendered at 1440 × 810, six source frames per second, then optical-flow interpolation within each shot to 24 fps and upscale to 1080p. The editable cinematic retains the camera animation at 24 fps and 1920 × 1080 for a future full-frame, higher-sample render. This is a first concept film rather than final photorealistic marketing output.
## Walk in Unreal Engine
Open `SamfordHomestead/SamfordHomestead.uproject` in Unreal Engine 5.8.2 on this Mac. Allow the first shader load to finish. The homestead level opens automatically.
1. Press the green Play button, then click inside the viewport.
2. Use W/A/S/D to walk and the mouse to look around.
3. Escape stops play. Shift+F1 releases the mouse while playing.
4. Start on the main verandah, walk into the kitchen/living room or down the front steps to the garden. The curved covered walkway leads to the meditation room.
The pawn uses Epic's bundled ArchVisCharacter plugin. It has a human-scale collision capsule and walking movement rather than free flight. The new project contains the entire site as grouped meshes with transferred material maps, collision, lights and presentation cameras. Python Editor Script, Editor Scripting Utilities, ArchVisCharacter, Sequencer Scripting and Movie Render Pipeline are bundled plugins enabled in the project; no external paid plugin is required.
This is an editable editor-play prototype, not a packaged application or browser-hosted simulation. Some large mesh groups and procedural material approximations will need finer optimization and art direction before production delivery. Open presentation faces are retained to match the Blender concept. Unbuilt pavilions have no collision. There are no interactive doors, product switches, VR controls or construction-stage simulation yet.
## Blender files
- `samford-solarpunk-presentation-tour.blend`: current enhanced whole-site scene, Cycles 2560 × 1440, all 31 inspection-tour chapters. FAST TOUR is the lightweight viewport; DETAIL contains the editable render geometry.
- `samford-client-cinematic.blend`: separate 24-second, eight-shot camera edit with the detailed geometry and no inspection captions. The opening/closing cards and film labels are added during encoding.
Changes: finer natural-finish variation and roughness, 420 softened construction/furniture edges, original woven cane kitchen pendants, stoneware table settings, a woven living rug and loose cushions, visible track/bolt details, fuller curved-leaf planting, and mineral chips along garden paths. Building positions remain unchanged.
## Continue development
`source/` contains the Blender enhancement, export, render, encode and verification scripts. `transfer/` contains the full-site FBX, portable textures and a manifest. Unreal setup and validation scripts live under `SamfordHomestead/Content/Python/`. The previous meditation-only Unreal study remains a separate historical project.
Next useful work: choose a surveyed property; refine human-scale access and all room routes; replace illustrative vegetation and some material approximations with selected production assets; optimize mesh grouping, LODs and collision; add optional closed-façade mode and interactive doors; author an Unreal Sequencer film; render a final full-frame presentation; package and test a client application for their target computer. LiDAR import and construction coordination remain later stages.
The design has no supplied cadastral boundary, surveyed terrain or engineering documentation. Treat the video and walkable model as concept communication, not an approved or site-verified construction proposal.
Final presentation review also repaired the kitchen mixer riser, which had been hidden by the clay lining, and replaced the obstructed exterior bedroom film angle with an interior view. These adjustments are retained in the final Blender model/cinematic and in the Unreal map. The base FBX is supplemented by `fix_tap_connection.py` for the final copper riser.

View File

@@ -0,0 +1,14 @@
import bpy,json,os
from mathutils import Matrix
s=bpy.context.scene
r={'collections':{c.name:len(c.objects) for c in bpy.data.collections},'materials':[],'objects':[],'cameras':[]}
for m in bpy.data.materials:
p=next((n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED'),None) if m.node_tree else None
r['materials'].append({'name':m.name,'color':list(m.diffuse_color),'roughness':p.inputs['Roughness'].default_value if p else None,'nodes':[n.type for n in m.node_tree.nodes] if m.node_tree else []})
for o in s.objects:
if o.type=='MESH' and not o.name.startswith('FAST'):
r['objects'].append({'name':o.name,'loc':list(o.location),'dim':list(o.dimensions),'mats':[m.name if m else '' for m in o.data.materials],'polys':len(o.data.polygons),'collections':[c.name for c in o.users_collection]})
for f in [1,121,241,361,481,601,721,841,961,1081,1201,1321,1441,1561,1681,1801,1921,2041,2161,2281,2401,2521,2641,2761,2881,3001,3121,3241,3361,3481,3601]:
s.frame_set(f);c=s.camera;r['cameras'].append({'f':f,'pos':list(c.location),'rot':list(c.rotation_euler),'lens':c.data.lens})
json.dump(r,open(os.path.dirname(__file__)+'/../scene-audit.json','w'),indent=2)
print('AUDIT',len(r['objects']),len(r['materials']))

View File

@@ -0,0 +1,24 @@
from pathlib import Path
import subprocess,json
from PIL import Image,ImageDraw,ImageFont
O=Path(__file__).resolve().parents[1];V=O/'video';F=V/'frames6';parts=V/'parts';parts.mkdir(exist_ok=True)
font='/System/Library/Fonts/Supplemental/Arial.ttf';bold='/System/Library/Fonts/Supplemental/Arial Bold.ttf'
def run(a):subprocess.run(['ffmpeg','-hide_banner','-loglevel','error','-y',*a],check=True)
labels=['A home held together by its garden','Open living and a shaded verandah','Natural materials / a shared kitchen','Gather around the outdoor table','A separate private sleeping wing','A quiet place for meditation','Growing food and living with the landscape','Leave space for the years ahead']
for i,label in enumerate(labels):
frames=list(F.glob('shot%02d_*.jpg'%(i+1)));assert len(frames)==18,(i,len(frames))
cached=parts/('shot%02d.mp4'%(i+1))
if cached.exists() and cached.stat().st_mtime>max(f.stat().st_mtime for f in frames):continue
overlay=Image.new('RGBA',(1920,1080),(0,0,0,0));d=ImageDraw.Draw(overlay);d.rectangle((0,960,1920,1080),fill=(0,0,0,82));d.text((64,992),label,font=ImageFont.truetype(font,30),fill='white');labelpath=parts/('label%02d.png'%(i+1));overlay.save(labelpath)
vf="[0:v]tpad=stop_mode=clone:stop_duration=0.5,minterpolate=fps=24:mi_mode=mci:mc_mode=aobmc:vsbmc=1,trim=duration=3,setpts=PTS-STARTPTS,scale=1920:1080:flags=lanczos[base];[base][1:v]overlay=0:0,format=yuv420p[v]"
run(['-framerate','6','-i',str(F/('shot%02d_%%03d.jpg'%(i+1))),'-i',str(labelpath),'-filter_complex',vf,'-map','[v]','-t','3','-an','-c:v','libx264','-preset','medium','-crf','18','-r','24',str(parts/('shot%02d.mp4'%(i+1)))])
for name,title,subtitle in [('opening','SAMFORD VALLEY','A home that grows with you'),('closing','Start simple. Leave room to grow.','DESIGN CONCEPT / SEPTEMBER 2026')]:
img=F/('shot01_000.jpg' if name=='opening' else 'shot08_017.jpg')
overlay=Image.new('RGBA',(1920,1080),(20,37,30,168));d=ImageDraw.Draw(overlay);d.text((960,445),title,font=ImageFont.truetype(bold,68 if name=='opening' else 54),fill='white',anchor='mt');d.text((960,550),subtitle,font=ImageFont.truetype(font,30),fill=(229,221,201),anchor='mt');labelpath=parts/(name+'.png');overlay.save(labelpath)
vf="[0:v]scale=1920:1080[base];[base][1:v]overlay=0:0,fade=t=in:st=0:d=0.3,fade=t=out:st=2.7:d=0.3,format=yuv420p[v]"
run(['-loop','1','-i',str(img),'-i',str(labelpath),'-t','3','-filter_complex',vf,'-map','[v]','-an','-c:v','libx264','-preset','medium','-crf','18','-r','24',str(parts/(name+'.mp4'))])
order=['opening']+['shot%02d'%(i+1) for i in range(8)]+['closing'];lst=V/'concat.txt';lst.write_text('\n'.join("file '%s'"%(parts/(name+'.mp4')) for name in order))
output=V/'Samford-Valley-Concept-Film-v007.mp4';run(['-f','concat','-safe','0','-i',str(lst),'-c','copy','-movflags','+faststart',str(output)])
probe=json.loads(subprocess.check_output(['ffprobe','-v','error','-show_streams','-show_format','-of','json',str(output)]));stream=probe['streams'][0];assert stream['width']==1920 and stream['height']==1080 and 29.9<float(probe['format']['duration'])<30.1
(V/'video-verification.json').write_text(json.dumps({'status':'PASS','output':output.name,'width':1920,'height':1080,'fps':stream['r_frame_rate'],'duration_seconds':probe['format']['duration'],'codec':stream['codec_name'],'size_bytes':output.stat().st_size,'source':'Animated Cycles renders, 1440x810 at 6 source frames/s, per-shot optical-flow interpolation to 24 fps and upscale to 1080p. No audio.'},indent=2))
run(['-i',str(output),'-vf','fps=1/3,scale=480:270,tile=5x2','-frames:v','1',str(V/'film-contact-sheet.jpg')]);print('CLIENT_FILM_READY',output,flush=True)

View File

@@ -0,0 +1,152 @@
import bpy,os,json,math,random
from mathutils import Matrix,Vector
from math import sin,cos,pi
random.seed(712);O=os.path.abspath(os.path.join(os.path.dirname(__file__),'..'));s=bpy.context.scene;detail=bpy.data.collections['DETAIL - full geometry for renders'];fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
c=bpy.data.collections.new('37 Presentation - crafted details and fuller planting');detail.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]
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'];ceramic=bpy.data.materials['Ceramic tea bowls warm white']
rattan=textured('Handwoven cane - fine warm fibres',(.22,.135,.062),(.48,.34,.17),(8,100,25),.72,.002)
terracotta=textured('Unglazed terracotta - fine mineral surface',(.22,.092,.044),(.38,.19,.10),(60,60,60),.86,.0015)
pebble=textured('Path aggregate - warm river stone',(.18,.17,.14),(.37,.34,.26),(25,25,25),.86,.002)
print('PHASE # Existing anisotropic',flush=True)
# Existing anisotropic grain retains direction; subtle roughness variation and random board tone.
material_changes=[]
for m in list(bpy.data.materials):
if not m.node_tree or m.name.startswith('VIEW'):continue
p=next((n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED'),None)
if not p:continue
ns=m.node_tree.nodes;ls=m.node_tree.links
if any(k in m.name.lower() for k in ['hardwood','spotted gum','timber','bamboo']):
old=p.inputs['Base Color'].links[0].from_socket if p.inputs['Base Color'].is_linked else None
if old:
oi=ns.new('ShaderNodeObjectInfo');ramp=ns.new('ShaderNodeValToRGB');ramp.color_ramp.elements[0].color=(.73,.70,.65,1);ramp.color_ramp.elements[1].color=(1,1,.98,1);ls.new(oi.outputs['Random'],ramp.inputs[0]);mix=ns.new('ShaderNodeMixRGB');mix.blend_type='MULTIPLY';mix.inputs[0].default_value=.30;ls.new(old,mix.inputs[1]);ls.new(ramp.outputs[0],mix.inputs[2]);ls.new(mix.outputs[0],p.inputs['Base Color'])
p.inputs['Roughness'].default_value=.57;material_changes.append(m.name)
if any(k in m.name.lower() for k in ['clay plaster','concrete','sandstone','marble','linen','textile']):
tex=ns.new('ShaderNodeTexNoise');tex.inputs['Scale'].default_value=135;tex.inputs['Detail'].default_value=2
ramp=ns.new('ShaderNodeMapRange');base=p.inputs['Roughness'].default_value;ramp.inputs['To Min'].default_value=max(.20,base-.12);ramp.inputs['To Max'].default_value=min(.96,base+.08);ls.new(tex.outputs['Fac'],ramp.inputs['Value']);ls.new(ramp.outputs[0],p.inputs['Roughness']);material_changes.append(m.name)
print('PHASE # Accurate small',flush=True)
# Accurate small highlights on edges and softer upholstered furniture.
beveled=0
for ob in list(detail.all_objects):
if ob.type!='MESH' or len(ob.data.polygons)>12 or ob.modifiers:continue
if not ob.data.materials:continue
names=' '.join(m.name for m in ob.data.materials if m)
if any(x in names.lower() for x in ['timber','hardwood','clay','marble','concrete','linen']):
ds=sorted(ob.dimensions)
if ds[0]<.018:continue
width=min(.004,ds[0]*.15)
if 'sofa' in ob.name.lower() or 'cushion' in ob.name.lower():width=min(.065,ds[0]*.35)
mod=ob.modifiers.new('Presentation softened real-world edge','BEVEL');mod.width=width;mod.segments=3;ob.modifiers.new('Presentation weighted corner normals','WEIGHTED_NORMAL');beveled+=1
print('PHASE # Simple lathed',flush=True)
# Simple lathed ceramic and woven forms, all original procedural assets.
def lathe(n,xy,profile,ma,sides=48):
vs=[];fs=[]
for z,r in profile:
for i in range(sides):a=i*2*pi/sides;vs.append((xy[0]+r*cos(a),xy[1]+r*sin(a),z))
for j in range(len(profile)-1):
for i in range(sides):k=j*sides+i;l=j*sides+(i+1)%sides;fs.append((k,l,l+sides,k+sides))
ob=mesh(n,vs,fs,ma)
for p in ob.data.polygons:p.use_smooth=True
return ob
def curve(n,pts,r,ma):
cu=bpy.data.curves.new(n,'CURVE');cu.dimensions='3D';cu.bevel_depth=r;cu.bevel_resolution=1;sp=cu.splines.new('POLY');sp.points.add(len(pts)-1)
for p,co in zip(sp.points,pts):p.co=(*co,1)
cu.materials.append(ma);ob=bpy.data.objects.new(n,cu);c.objects.link(ob);return ob
print('PHASE # Cane pendant',flush=True)
# Cane pendant shades and warm concealed bulbs over kitchen island.
for x in [-4.37,-3.22]:
rod('Island pendant black suspension',(x,.1,3.32),(x,.1,4.10),.007,steel,sides=10)
profile=[(3.12,.29),(3.18,.30),(3.34,.25),(3.45,.12)]
for i in range(40):
a=i*2*pi/40;curve('Cane pendant vertical weave',[(x+r*cos(a),.1+r*sin(a),z) for z,r in profile],.0038,rattan)
for j in range(17):
t=j/16;z=3.12+.33*t;r=.30-.18*t*t;curve('Cane pendant horizontal weave',[(x+r*cos(i*2*pi/64),.1+r*sin(i*2*pi/64),z) for i in range(65)],.003,rattan)
bulb=mat('Pendant warm diffuser '+str(x),(.9,.65,.35),.7);p=bulb.node_tree.nodes['Principled BSDF'];p.inputs['Emission Color'].default_value=(1,.62,.28,1);p.inputs['Emission Strength'].default_value=2
lathe('Warm pendant diffuser',(x,.1),[(3.18,.02),(3.20,.055),(3.26,.055),(3.28,.01)],bulb)
ld=bpy.data.lights.new('Warm island practical light','POINT');ld.energy=12;ld.color=(1,.72,.45);ld.shadow_soft_size=.14;ob=bpy.data.objects.new(ld.name,ld);c.objects.link(ob);ob.location=(x,.1,3.14)
print('PHASE # Stoneware',flush=True)
# Stoneware table settings make dimensions legible at eye level.
for x in [-.65,.1,.85]:
for y in [-.02,.63]:
lathe('Handmade stoneware dinner plate',(x,y),[(1.921,0),(1.922,.085),(1.94,.118),(1.947,.118),(1.938,.083),(1.937,0)],ceramic)
box('Folded flax table napkin',(x+.19,y,1.928),(.12,.16,.014),linen,.005)
lathe('Handmade tea cup',(x-.17,y+.04),[(1.92,.026),(2.00,.033),(2.004,.037),(1.92,.033)],ceramic,32)
lathe('Dining table terracotta bud vase',(.1,.30),[(1.92,.04),(1.95,.065),(2.07,.057),(2.13,.025),(2.15,.025)],terracotta)
for dx,dy in [(-.04,.015),(.045,.025),(0,-.03)]:rod('Vase dried grass stem',(.1,.30,2.06),(.1+dx,.30+dy,2.36),.002,rattan,sides=6)
print('PHASE # Low woven',flush=True)
# Low woven living rug, extra loose cushions with softened edges.
box('Living woven jute rug',(3.2,1.10,1.136),(3.25,2.15,.016),rattan,.008)
for x,a in [(2.47,-.12),(3.9,.10)]:
ob=box('Loose linen sofa pillow',(x,-.75,1.88),(.48,.17,.43),linen,.065);ob.rotation_euler[1]=a
print('PHASE # Stainless',flush=True)
# Stainless track rails and small timber fixings visible in close camera views.
for y in [2.89,3.05,3.21]:box('Sliding door stainless track inset',(0,y,1.152),(11.82,.013,.009),bpy.data.materials['Appliance brushed metal'],.002)
for x in [-6,-3,0,3,6]:
for z in [1.3,3.43]:
for dx in [-.032,.032]:rod('Verandah exposed bolt head',(x+dx,5.466,z),(x+dx,5.475,z),.008,steel,sides=6)
print('PHASE # Richer planted',flush=True)
# Richer planted borders: broad curved leaves, fine flowering stems, aggregate along paths.
layout=json.load(open(O+'/../samford-retreat-garage/buddha-garden-layout.json'))
def ground(x,y):return -.024*y+.004*x+.12*sin(x*.19)*cos(y*.14)+.055*sin(x*.72+y*.32)
leafm=[bpy.data.materials['Garden leaf '+str(i)] for i in range(4)];flower=bpy.data.materials['Garden flower soft ivory']
vs=[];fs=[];mi=[]
for bed in layout['beds']:
x,y=bed['center'];rx,ry=bed['radii']
for j in range(max(8,int(rx*ry*14))):
a=random.random()*2*pi;r=math.sqrt(random.random())*.9;xx=x+rx*r*cos(a);yy=y+ry*r*sin(a);zz=ground(xx,yy)+.10
for k in range(7):
a=random.random()*2*pi;L=random.uniform(.18,.47);w=L*.15;off=len(vs)
for t in [0,.25,.5,.75,1]:
reach=L*t;z=zz+L*(sin(t*pi*.65)*.65+.08*t);width=w*sin(pi*t)
for sg in [-1,0,1]:vs.append((xx+cos(a)*reach-sin(a)*width*sg,yy+sin(a)*reach+cos(a)*width*sg,z+(.014*sin(pi*t) if sg==0 else 0)))
for k in range(4):
for h in range(2):idx=off+k*3+h;fs.append((idx,idx+1,idx+4,idx+3));mi.append(random.randrange(4))
ob=mesh('Fuller garden borders - curved lanceolate leaves',vs,fs)
for m in leafm:ob.data.materials.append(m)
ob.data.polygons.foreach_set('material_index',mi);ob.data.polygons.foreach_set('use_smooth',[True]*len(fs))
print('PHASE # Small mineral',flush=True)
# Small mineral chips follow the path ribbons, with low height to preserve walking.
vs=[];fs=[]
for path in layout['paths']:
if path['kind']=='roofed walk' or 'VEHICLE' in path['name']:continue
pts=path['points']
for i in range(1,len(pts)-1,2):
q=Vector(pts[i]);v=(Vector(pts[i+1])-Vector(pts[i-1])).normalized();side=Vector((-v.y,v.x))
for j in range(4):
pos=q+side*random.uniform(-.46,.46)*path['width'];z=ground(*pos)+.095;rx=random.uniform(.012,.038);ry=random.uniform(.009,.025);off=len(vs)
vs.extend([(pos.x+rx*cos(k*pi/3),pos.y+ry*sin(k*pi/3),z+random.uniform(0,.009)) for k in range(6)]+[(pos.x,pos.y,z+.013)])
fs.extend((off+k,off+(k+1)%6,off+6) for k in range(6))
mesh('Garden paths - scattered mineral aggregate',vs,fs,pebble)
print('PHASE # A more neutral',flush=True)
# A more neutral, readable morning exposure; practical lights remain gentle.
s.view_settings.view_transform='AgX';s.view_settings.look='AgX - Medium High Contrast';s.view_settings.exposure=.20
s.cycles.samples=96;s.cycles.use_denoising=True;s.render.resolution_x=2560;s.render.resolution_y=1440;s.render.resolution_percentage=100
print('PHASE # Refresh additional',flush=True)
# Refresh additional detail proxy (existing geometry only changed through render modifiers).
print('PHASE detail.hide_viewport=False',flush=True)
detail.hide_viewport=False;bpy.context.view_layer.update();dg=bpy.context.evaluated_depsgraph_get();vs=[];fs=[];mi=[];mats=[];smooth=[]
for ob in c.objects:
if ob.type not in ['MESH','CURVE']:continue
ev=ob.evaluated_get(dg);me=ev.to_mesh();off=len(vs);mw=Matrix.LocRotScale(ob.location,ob.rotation_euler.to_quaternion(),ob.scale);vs.extend(tuple(mw@v.co) for v in me.vertices)
real_mats=[bpy.data.materials.get(m.name) for m in me.materials]
for m in real_mats:
if m not in mats:mats.append(m)
for p in me.polygons:fs.append(tuple(off+i for i in p.vertices));mi.append(mats.index(real_mats[p.material_index]));smooth.append(p.use_smooth)
ev.to_mesh_clear()
me=bpy.data.meshes.new('Presentation detail proxy');me.from_pydata(vs,[],fs);me.update()
for m in mats:me.materials.append(m)
me.polygons.foreach_set('material_index',mi);me.polygons.foreach_set('use_smooth',smooth);ob=bpy.data.objects.new('FAST | Crafted details and fuller gardens',me);fast.objects.link(ob)
detail.hide_viewport=True
s['Current revision']='v007 — presentation detail, textured materials, fuller garden borders and a separate cinematic/Unreal walkthrough prototype.'
print('PHASE s.frame_set(1);bpy.ops.wm.save',flush=True)
s.frame_set(1);bpy.ops.wm.save_as_mainfile(filepath=O+'/samford-solarpunk-presentation-tour.blend')
json.dump({'version':7,'beveled_objects':beveled,'materials_refined':material_changes,'new_detail_objects':len(c.objects),'new_leaf_faces':len(bpy.data.objects['Fuller garden borders - curved lanceolate leaves'].data.polygons),'tour_chapters':31},open(O+'/blender-enhancement-checks.json','w'),indent=2)
# Actual Cycles stills for presentation review.
for ob in bpy.data.collections['22 PLAY - guided camera tour'].objects:
if ob.type=='FONT':ob.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=48;s.render.resolution_percentage=75
for name,f in [('presentation-overview',45),('presentation-kitchen',3520),('presentation-garden',3270)]:s.frame_set(f);s.render.filepath=O+'/renders/'+name+'.png';bpy.ops.render.render(write_still=True)
print('PRESENTATION_READY',beveled,len(c.objects),flush=True)

View File

@@ -0,0 +1,57 @@
import bpy,os,json,re,math
from mathutils import Vector,Matrix
from collections import defaultdict
O=os.path.abspath(os.path.join(os.path.dirname(__file__),'..'));OUT=O+'/transfer';os.makedirs(OUT+'/Textures',exist_ok=True);source=bpy.context.scene;detail=bpy.data.collections['DETAIL - full geometry for renders'];detail.hide_viewport=False;source.frame_set(1);bpy.context.view_layer.update();deps=bpy.context.evaluated_depsgraph_get()
objects=[o for o in detail.all_objects if o.type in ['MESH','CURVE'] and not o.hide_render and not any(c.name.startswith(('15 ','22 ','30 ')) for c in o.users_collection)]
used=[]
for o in objects:
for m in o.data.materials:
if m and not m.name.startswith('VIEW') and m not in used:used.append(m)
new=bpy.data.scenes.new('Homestead Unreal transfer');new.unit_settings.system='METRIC';new.unit_settings.scale_length=1;bpy.context.window.scene=new
manifest={'version':7,'source_blend':'samford-solarpunk-presentation-tour.blend','coordinate_transform':'UE centimetres: (100*x,-100*y,100*z)','objects':[],'materials':{},'source_objects':len(objects)}
# Bake portable colour and tangent normal maps using the existing procedural shaders.
new.render.engine='CYCLES';new.cycles.samples=1;new.cycles.device='CPU';new.render.bake.margin=4
bpy.ops.mesh.primitive_plane_add(size=2);tile=bpy.context.object
replacement={}
for i,m in enumerate(used):
key='M_%03d_'%i+re.sub('[^a-zA-Z0-9_]','_',m.name)[:65];p=next((n for n in m.node_tree.nodes if n.type=='BSDF_PRINCIPLED'),None) if m.node_tree 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':float(p.inputs['Emission Strength'].default_value) if p else 0,'transmission':float(p.inputs['Transmission Weight'].default_value) if p else 0}
exported=bpy.data.materials.new(key);exported.diffuse_color=base;replacement[m]=exported
if p and p.inputs['Base Color'].is_linked:
bake=m.copy();tile.data.materials.clear();tile.data.materials.append(bake);ns=bake.node_tree.nodes;ls=bake.node_tree.links;bp=next(n for n in ns if n.type=='BSDF_PRINCIPLED');out=next(n for n in ns 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=ns.new('ShaderNodeTexImage');target.image=im;ns.active=target;em=ns.new('ShaderNodeEmission');ls.new(bp.inputs['Base Color'].links[0].from_socket,em.inputs['Color']);ls.new(em.outputs[0],out.inputs['Surface']);bpy.context.view_layer.objects.active=tile;tile.select_set(True);bpy.ops.object.bake(type='EMIT');im.save();record['base_color_texture']='Textures/'+key+'_BaseColor.png'
if bp.inputs['Normal'].is_linked:
nm=bpy.data.images.new(key+'_Normal',width=512,height=512);nm.colorspace_settings.name='Non-Color';nm.filepath_raw=OUT+'/Textures/'+key+'_Normal.png';nm.file_format='PNG';target.image=nm;ns.active=target;ls.new(bp.outputs[0],out.inputs['Surface']);bpy.ops.object.bake(type='NORMAL');nm.save();record['normal_texture']='Textures/'+key+'_Normal.png'
bpy.data.materials.remove(bake)
manifest['materials'][key]=record
print('BAKED',i,key,flush=True)
bpy.data.objects.remove(tile,do_unlink=True)
# Partition by collection and material: static world transforms, proper normals and box UVs.
groups={}
for idx,o in enumerate(objects):
ev=o.evaluated_get(deps);me=ev.to_mesh();mw=Matrix.LocRotScale(o.location,o.rotation_euler.to_quaternion(),o.scale)
category=next((c.name for c in o.users_collection if c.name!='DETAIL - full geometry for renders'),'Other');prefix=re.sub('[^a-zA-Z0-9_]','_',category)[:55]
verts=[tuple(mw@v.co) for v in me.vertices]
if not verts:ev.to_mesh_clear();continue
lo=[min(v[k] for v in verts) for k in range(3)];hi=[max(v[k] for v in verts) for k in range(3)]
for mi,em in enumerate(me.materials):
m=bpy.data.materials.get(em.name) if em else None
if m not in replacement:continue
polys=[p for p in me.polygons if p.material_index==mi]
if not polys:continue
key=(prefix,replacement[m].name);g=groups.setdefault(key,{'verts':[],'faces':[],'uvs':[],'smooth':[],'sources':[]});g['sources'].append(o.name)
off=len(g['verts']);g['verts'].extend(verts)
for p in polys:
g['faces'].append(tuple(off+i for i in p.vertices));g['smooth'].append(p.use_smooth)
normal=mw.to_3x3()@p.normal;drop=max(range(3),key=lambda i:abs(normal[i]));axes=[i for i in range(3) if i!=drop]
for vi in p.vertices:g['uvs'].append(tuple((verts[vi][k]-lo[k])/max(.08,hi[k]-lo[k]) for k in axes))
ev.to_mesh_clear()
if idx%800==0:print('GEOMETRY',idx,flush=True)
for number,((cat,matname),g) in enumerate(groups.items()):
name='SM_%03d_'%number+cat;me=bpy.data.meshes.new(name);me.from_pydata(g['verts'],[],g['faces']);me.update();me.materials.append(bpy.data.materials[matname]);me.polygons.foreach_set('use_smooth',g['smooth']);uv=me.uv_layers.new();uv.data.foreach_set('uv',[v for q in g['uvs'] for v in q]);ob=bpy.data.objects.new(name,me);new.collection.objects.link(ob)
noncoll=cat.startswith(('08_','09_','24_')) or any(k in matname.lower() for k in ['leaf','grass','flower','glazing','shadow','haze','flame','annotation']) or all(any(k in n.lower() for k in ['leaves','aggregate','weave','pendant','plate','cup','stem','vase','track inset','bolt head']) for n in g['sources'])
manifest['objects'].append({'name':name,'material':matname,'category':cat,'collision':not noncoll,'vertices':len(g['verts']),'faces':len(g['faces']),'source_count':len(g['sources'])})
bpy.ops.object.select_all(action='SELECT')
bpy.ops.export_scene.fbx(filepath=OUT+'/SamfordHomestead.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')
json.dump(manifest,open(OUT+'/homestead-manifest.json','w'),indent=2)
print('HOMESTEAD_EXPORT_READY',len(groups),len(used),flush=True)

View File

@@ -0,0 +1,22 @@
import bpy,os,json
from mathutils import Vector
from math import sin,cos,pi
O=os.path.abspath(os.path.join(os.path.dirname(__file__),'..'));s=bpy.context.scene;mode='cinematic' if 'cinematic' in bpy.data.filepath else 'model';c=bpy.data.collections['37 Presentation - crafted details and fuller planting'];fast=bpy.data.collections['00 FAST TOUR - lightweight viewport']
a=Vector((-4.32,-2.74,2.067));b=Vector((-4.32,-2.74,2.475));q=(b-a).to_track_quat('Z','Y');N=24
vs=[tuple(a+q@Vector((.014*cos(i*2*pi/N),.014*sin(i*2*pi/N),z))) for z in [0,(b-a).length] for i in range(N)];fs=[tuple(reversed(range(N))),tuple(range(N,2*N))]+[(i,(i+1)%N,(i+1)%N+N,i+N) for i in range(N)]
me=bpy.data.meshes.new('Connected copper mixer riser');me.from_pydata(vs,[],fs);me.update();me.materials.append(bpy.data.materials['Copper cookware - brushed warm patina'])
for p in me.polygons:p.use_smooth=len(p.vertices)==4
ob=bpy.data.objects.new('Kitchen mixer - visible connected copper riser',me);c.objects.link(ob);proxy=bpy.data.objects.new('FAST | Connected kitchen mixer riser',me);fast.objects.link(proxy);s.frame_set(1);bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
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.use_denoising=True;s.render.use_persistent_data=True;s.render.resolution_percentage=75
if mode=='model':
for c in ['22 PLAY - guided camera tour','15 Expansion labels - toggle to hide']:
for ob in bpy.data.collections[c].objects:
if ob.type=='FONT':ob.hide_render=True
s.cycles.samples=48;s.frame_set(3520);s.render.filepath=O+'/renders/presentation-kitchen.png';bpy.ops.render.render(write_still=True)
r=json.load(open(O+'/blender-enhancement-checks.json'));r['new_detail_objects']+=1;r['tap_connection_corrected']=True;json.dump(r,open(O+'/blender-enhancement-checks.json','w'),indent=2)
else:
s.cycles.samples=8;s.cycles.adaptive_threshold=.12;s.render.image_settings.file_format='JPEG';s.render.image_settings.quality=95
for j in range(18):s.frame_set(145+j*4);s.render.filepath=O+'/video/frames6/shot03_%03d.jpg'%j;bpy.ops.render.render(write_still=True)
print('TAP_CONNECTION_REPAIRED',mode,flush=True)

View File

@@ -0,0 +1,37 @@
import bpy,os,json,math
from mathutils import Vector
O=os.path.abspath(os.path.join(os.path.dirname(__file__),'..'));s=bpy.context.scene
for c in ['22 PLAY - guided camera tour','15 Expansion labels - toggle to hide']:
for o in bpy.data.collections[c].objects:
if o.type=='FONT':o.hide_render=True
cam=s.camera;cam.animation_data_clear();cam.data.animation_data_clear();cam.data.dof.use_dof=False
# Eight compact camera movements; no fly-through through walls or furniture.
shots=[
('A home in the garden',(35,43,29),(32,40,27),(0,-3,1),38),
('Timber verandah and open living',(7,11,3.3),(5.8,9.7,3.1),(0,1.5,2.1),31),
('Kitchen / natural materials',(1.5,2.35,2.85),(.7,2.1,2.75),(-3.5,-1.3,2.15),27),
('Room for shared meals',(17,9,4.3),(16,8.2,4.0),(10,3.5,1.9),36),
('A private sleeping wing',(-6.4,.7,4.2),(-7.3,.4,3.9),(-12,-5,2),34),
('Meditation in the garden',(-6.9,18.5,3),(-7.7,17.7,2.6),(-12.4,15.35,.65),42),
('Growing food / collecting water',(34,15,17),(32,13,15),(21,-3,1),37),
('Space to grow over time',(23,33,20),(20,30,18),(0,13,1),35)]
s.render.fps=24;s.frame_start=1;s.frame_end=8*72
for i,(label,a,b,target,lens) in enumerate(shots):
start=i*72+1;end=(i+1)*72
for f,p in [(start,a),(end,b)]:
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 owner in [cam,cam.data]:
for layer in owner.animation_data.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='LINEAR'
s.render.engine='CYCLES';s.cycles.samples=12;s.cycles.use_denoising=True;s.cycles.use_adaptive_sampling=True;s.cycles.adaptive_threshold=.08;s.render.resolution_x=1920;s.render.resolution_y=1080;s.render.resolution_percentage=100;s.render.image_settings.file_format='PNG';s.render.filepath=O+'/video/frames/frame_';s.render.film_transparent=False
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.use_persistent_data=True;s.frame_set(1)
bpy.ops.wm.save_as_mainfile(filepath=O+'/samford-client-cinematic.blend')
json.dump([dict(title=sh[0],start=i*72+1,end=(i+1)*72,seconds=3) for i,sh in enumerate(shots)],open(O+'/video/cinematic-shots.json','w'),indent=2)
# Compare a real-time preview frame before choosing the final video renderer.
s.render.engine='CYCLES';s.cycles.samples=12;s.frame_set(145);s.render.filepath=O+'/renders/cinematic-kitchen-test.png';bpy.ops.render.render(write_still=True)
print('CINEMATIC_PREPARED',flush=True)

View File

@@ -0,0 +1,13 @@
import bpy,os
from mathutils import Vector
O=os.path.abspath(os.path.join(os.path.dirname(__file__),'..'));s=bpy.context.scene;cam=s.camera
for f,p in [(289,(-10.7,-6.95,3.30)),(360,(-10.9,-6.75,3.15))]:
s.frame_set(f);cam.location=p;cam.rotation_euler=(Vector((-12.5,-5,2.02))-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=23;cam.data.keyframe_insert('lens',frame=f)
s.frame_set(1);bpy.ops.wm.save_as_mainfile(filepath=O+'/samford-client-cinematic.blend')
s.render.resolution_percentage=75;s.cycles.samples=8;s.cycles.use_denoising=True;s.cycles.adaptive_threshold=.12;s.render.use_persistent_data=True;s.render.image_settings.file_format='JPEG';s.render.image_settings.quality=95
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 j in range(18):
s.frame_set(289+j*4);s.render.filepath=O+'/video/frames6/shot05_%03d.jpg'%j;bpy.ops.render.render(write_still=True)
print('BEDROOM_SHOT_REFINED',flush=True)

View File

@@ -0,0 +1,16 @@
import bpy,os,time,json
O=os.path.abspath(os.path.join(os.path.dirname(__file__),'..'));s=bpy.context.scene
# Six source frames per second, interpolated per shot to the 24 fps presentation master.
s.render.resolution_percentage=75;s.cycles.samples=8;s.cycles.adaptive_threshold=.12;s.render.use_persistent_data=True;s.render.image_settings.file_format='JPEG';s.render.image_settings.quality=95
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'
started=time.time()
for shot in range(8):
for j in range(18):
f=shot*72+1+j*4;path=O+'/video/frames6/shot%02d_%03d.jpg'%(shot+1,j)
if os.path.exists(path):continue
s.frame_set(f);s.render.filepath=path;bpy.ops.render.render(write_still=True)
if j%12==0:print('FILM_PROGRESS',shot+1,j,round(time.time()-started),flush=True)
json.dump({'source_size':[1440,810],'source_fps':6,'shots':8,'source_frames':144,'seconds':24,'renderer':'Cycles','samples':8},open(O+'/video/render-checks.json','w'),indent=2)
print('FILM_FRAMES_READY',flush=True)

View File

@@ -0,0 +1,12 @@
import bpy,json,os
s=bpy.context.scene;O=os.path.abspath(os.path.join(os.path.dirname(__file__),'..'));names=[o.name for o in s.objects];r={}
r['required_features']={n:n in names for n in ['ISLAND - honed marble countertop','FAST | Garden Buddha statue','CONTAINER WATER - steel base frame','Living woven jute rug','Fuller garden borders - curved lanceolate leaves','FAST | Crafted details and fuller gardens']}
assert all(r['required_features'].values())
r['render']=[s.render.engine,s.render.resolution_x,s.render.resolution_y,s.render.resolution_percentage];assert r['render']==['CYCLES',2560,1440,100]
r['ghosts']=len(bpy.data.collections['24 LATER - Shadow spaces only'].objects);assert r['ghosts']==26
r['captions']=[];fonts=[o for o in bpy.data.collections['22 PLAY - guided camera tour'].objects if o.type=='FONT']
for f in range(1,3721,120):s.frame_set(f+20);visible=[o.data.body for o in fonts if o.scale.length>.5];assert len(visible)==1,(f,visible);r['captions']+=visible
assert len(r['captions'])==31 and s.frame_end==3720
r['no_ping_pong']=not any('table tennis' in n.lower() for n in names);assert r['no_ping_pong']
r['material_references_valid']=all(m in bpy.data.materials.values() for o in s.objects if o.type=='MESH' for m in o.data.materials if m);assert r['material_references_valid']
r['status']='PASS';json.dump(r,open(O+'/blender-verification.json','w'),indent=2);print('BLENDER_VERIFIED',flush=True)