Private
Public Access
Initiales Halloween-Projekt
This commit is contained in:
commit
e071fbad2a
36 files changed
+2220
No files matched your search
@@ -0,0 +1,50 @@
|
||||
"""Add a low synthetic vocal moan at the hand emergence and export via VSE."""
|
||||
import os, math, wave, array, random
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview','v4');random.seed(49)
|
||||
with wave.open(os.path.join(ROOT,'preview','effects.wav'),'rb') as w:
|
||||
rate=w.getframerate();samples=array.array('h',w.readframes(w.getnframes()))
|
||||
moan=[0.0]*len(samples);phase=0;filtered=0;start=13.04;length=3.15
|
||||
for i in range(int(length*rate)):
|
||||
t=i/rate;pitch=92-25*(t/length)+2.2*math.sin(t*6.5)
|
||||
phase+=math.tau*pitch/rate
|
||||
# Harmonic vowel with resonances, descending pitch, and breath roughness.
|
||||
value=0
|
||||
for h in range(1,15):
|
||||
freq=h*pitch;weight=.35*math.exp(-((freq-330)/160)**2)+.16*math.exp(-((freq-720)/210)**2)
|
||||
value+=weight*math.sin(h*phase)/math.sqrt(h)
|
||||
value+=.17*math.sin(phase)+.06*math.sin(phase*.5)
|
||||
filtered=.92*filtered+.08*random.uniform(-1,1)
|
||||
envelope=min(1,t/.16)*min(1,(length-t)/.65)*(.7+.3*math.sin(math.pi*t/length))
|
||||
idx=int(start*rate)+i
|
||||
if idx<len(moan):moan[idx]=envelope*(value*.72+filtered*.22)*(1+.08*math.sin(t*31))
|
||||
# Short, subdued echoes evoke a hollow cavity under the grave.
|
||||
output=[]
|
||||
for i,old in enumerate(samples):
|
||||
value=moan[i]
|
||||
for delay,gain in [(.19,.22),(.37,.12)]:
|
||||
index=i-int(delay*rate)
|
||||
if index>=0:value+=gain*moan[index]
|
||||
mixed=old/32768+value
|
||||
output.append(int(max(-.98,min(.98,mixed))*32767))
|
||||
assert max(abs(x) for x in output)<32767
|
||||
path=os.path.join(OUT,'effects_with_groan.wav')
|
||||
with wave.open(path,'wb') as w:w.setparams((1,2,rate,len(output),'NONE','not compressed'));w.writeframes(array.array('h',output).tobytes())
|
||||
print('GROAN: 13.04 to 16.19 seconds; peak',max(abs(x) for x in output)/32768)
|
||||
# When invoked with Blender, update a separate editable copy and export video.
|
||||
try:import bpy
|
||||
except ImportError:raise SystemExit(0)
|
||||
bpy.ops.wm.open_mainfile(filepath=os.path.join(OUT,'halloween_dark.blend'))
|
||||
s=bpy.context.scene
|
||||
for strip in list(s.sequence_editor.sequences):
|
||||
if strip.type=='SOUND':s.sequence_editor.sequences.remove(strip)
|
||||
s.sequence_editor.sequences.new_sound('Dark moan and ambience',path,channel=1,frame_start=1)
|
||||
bpy.ops.file.pack_all();bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_dark_groan.blend'))
|
||||
# Reuse the already rendered movie; do not render the 3D scene again.
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True);s=bpy.context.scene
|
||||
s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100;s.render.fps=24;s.frame_start=1;s.frame_end=480
|
||||
s.view_settings.view_transform='Standard';s.view_settings.look='Medium High Contrast' if False else 'None'
|
||||
s.sequence_editor_create();s.sequence_editor.sequences.new_movie('Existing rendered picture',os.path.join(OUT,'halloween_dark.mp4'),channel=1,frame_start=1)
|
||||
s.sequence_editor.sequences.new_sound('New mixed sound',path,channel=2,frame_start=1)
|
||||
s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4';s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='HIGH';s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192
|
||||
s.render.filepath=os.path.join(OUT,'halloween_dark_groan.mp4');bpy.ops.render.render(animation=True)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Inspect MP4 track metadata without external tools."""
|
||||
import json, struct, sys
|
||||
from pathlib import Path
|
||||
|
||||
def boxes(data):
|
||||
offset=0
|
||||
while offset+8<=len(data):
|
||||
size,kind=struct.unpack_from('>I4s',data,offset);header=8
|
||||
if size==1:size=struct.unpack_from('>Q',data,offset+8)[0];header=16
|
||||
if size==0:size=len(data)-offset
|
||||
if size<header or offset+size>len(data):raise ValueError('Invalid MP4 box')
|
||||
yield kind.decode('ascii'), data[offset+header:offset+size]
|
||||
offset+=size
|
||||
|
||||
def child(data,name):return next(body for kind,body in boxes(data) if kind==name)
|
||||
|
||||
def inspect(path):
|
||||
moov=child(Path(path).read_bytes(),'moov');result=[]
|
||||
for kind,trak in boxes(moov):
|
||||
if kind!='trak':continue
|
||||
mdia=child(trak,'mdia');hdlr=child(mdia,'hdlr');kind=hdlr[8:12].decode()
|
||||
mdhd=child(mdia,'mdhd')
|
||||
if mdhd[0]==0:scale,duration=struct.unpack_from('>II',mdhd,12)
|
||||
else:scale=struct.unpack_from('>I',mdhd,20)[0];duration=struct.unpack_from('>Q',mdhd,24)[0]
|
||||
stbl=child(child(mdia,'minf'),'stbl');stsz=child(stbl,'stsz');count=struct.unpack_from('>I',stsz,8)[0]
|
||||
desc=child(stbl,'stsd');entry=next(boxes(desc[8:]));seconds=duration/scale
|
||||
item={'type':kind,'codec':entry[0],'duration_seconds':seconds,'samples':count}
|
||||
if kind=='vide':
|
||||
width,height=struct.unpack_from('>HH',entry[1],24);item.update(width=width,height=height,fps=count/seconds)
|
||||
assert (width,height,count)==(1280,720,480),item
|
||||
assert abs(seconds-20)<.01,item
|
||||
result.append(item)
|
||||
assert any(t['type']=='soun' for t in result),'Missing audio track'
|
||||
return result
|
||||
if __name__=='__main__':print(json.dumps(inspect(sys.argv[1]),indent=2))
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
service_name=halloween-grab.service
|
||||
unit_path="/etc/systemd/system/$service_name"
|
||||
apply=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Richtet die Halloween-WebUI als systemd-Dienst auf dem Raspberry Pi ein.
|
||||
|
||||
Aufruf: bash tools/install_pi_service.sh [--apply]
|
||||
|
||||
Ohne --apply wird nur die geplante Unit gezeigt. --apply installiert sie mit
|
||||
sudo, aktiviert den Autostart und startet die WebUI sofort neu.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Fehler: %s\n' "$*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while (($#)); do
|
||||
case $1 in
|
||||
--apply) apply=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "Unbekanntes Argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
service_user=$(id -un)
|
||||
[[ $service_user =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || die 'Ungeeigneter Benutzername für systemd'
|
||||
[[ $service_user != root ]] || die 'Skript als normaler Pi-Benutzer ausführen, nicht als root'
|
||||
[[ $project_dir =~ ^/[a-zA-Z0-9_./-]+$ ]] || die 'Projektpfad enthält für die Unit ungeeignete Zeichen'
|
||||
[[ /$project_dir/ != *'/../'* && /$project_dir/ != *'/./'* ]] || die 'Projektpfad enthält . oder .. Segmente'
|
||||
|
||||
unit_contents=$(cat <<EOF
|
||||
# Managed by Halloween project: tools/install_pi_service.sh
|
||||
[Unit]
|
||||
Description=Halloween-Grab WebUI und Kamera
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$service_user
|
||||
WorkingDirectory=$project_dir
|
||||
ExecStart=$project_dir/.venv/bin/python $project_dir/run.py
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
)
|
||||
|
||||
printf 'Dienst: %s\nBenutzer: %s\nProjekt: %s\n' "$service_name" "$service_user" "$project_dir"
|
||||
if ! $apply; then
|
||||
printf '\nVorschau für %s:\n%s\n' "$unit_path" "$unit_contents"
|
||||
printf '\nMit --apply installieren, aktivieren und starten.\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ -f "$project_dir/run.py" ]] || die 'run.py fehlt im Projektordner'
|
||||
[[ -x "$project_dir/.venv/bin/python" ]] || die '.venv fehlt; zuerst SOFTWARE.md: Pi vorbereiten ausführen'
|
||||
"$project_dir/.venv/bin/python" -c 'import fastapi, uvicorn, PIL' \
|
||||
|| die 'Python-Abhängigkeiten fehlen; zuerst requirements.txt installieren'
|
||||
command -v sudo >/dev/null || die 'sudo fehlt'
|
||||
command -v systemctl >/dev/null || die 'systemctl fehlt'
|
||||
|
||||
if sudo test -e "$unit_path" &&
|
||||
! sudo grep -qF '# Managed by Halloween project: tools/install_pi_service.sh' "$unit_path"; then
|
||||
die "Vorhandene fremde Unit wird nicht überschrieben: $unit_path"
|
||||
fi
|
||||
|
||||
temp_unit=$(mktemp)
|
||||
trap 'rm -f -- "$temp_unit"' EXIT
|
||||
printf '%s\n' "$unit_contents" > "$temp_unit"
|
||||
sudo install -m 0644 -- "$temp_unit" "$unit_path"
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "$service_name"
|
||||
sudo systemctl restart "$service_name"
|
||||
if ! systemctl is-active --quiet "$service_name"; then
|
||||
die "Dienst läuft nicht; Diagnose: sudo journalctl -u $service_name -n 80 --no-pager"
|
||||
fi
|
||||
printf 'Autostart aktiviert und Dienst gestartet. Status: systemctl status %s\n' "$service_name"
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Improve the existing concept scene; render still examples only."""
|
||||
import bpy, math, random, os
|
||||
from mathutils import Vector
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview','v2');os.makedirs(OUT,exist_ok=True)
|
||||
bpy.ops.wm.open_mainfile(filepath=os.path.join(ROOT,'preview','halloween_preview.blend'))
|
||||
s=bpy.context.scene;s.eevee.taa_render_samples=32
|
||||
random.seed(104)
|
||||
def material(name,c,rough=.8):
|
||||
m=bpy.data.materials.new(name);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
|
||||
return m
|
||||
leafm=[material('Dry oak leaves '+str(i),c) for i,c in enumerate([(.20,.055,.012),(.32,.14,.032),(.10,.042,.013),(.40,.23,.075),(.16,.08,.025)])]
|
||||
shroud=bpy.data.objects['Breathing burial shroud'];base=shroud.data.shape_keys.key_blocks['Rest'];breath=shroud.data.shape_keys.key_blocks['Breath']
|
||||
# A real opening at the hand's emergence point, rather than intersecting cloth.
|
||||
import bmesh
|
||||
bm=bmesh.new();bm.from_mesh(shroud.data)
|
||||
for face in list(bm.faces):
|
||||
c=face.calc_center_median()
|
||||
if .48<c.x<.80 and -1.35<c.y<-.96:bm.faces.remove(face)
|
||||
bm.to_mesh(shroud.data);bm.free()
|
||||
# Leaf litter follows the animated cloth with its own synchronized shape key.
|
||||
verts=[];faces=[];indices=[];lifts=[]
|
||||
def surface(x,y):
|
||||
u=(x+.8)/1.6*28;v=(y+1.35)/2.7*44;i=min(27,max(0,int(u)));j=min(43,max(0,int(v)));a=u-i;b=v-j
|
||||
zs=[base.data[k].co.z for k in [j*29+i,j*29+i+1,(j+1)*29+i,(j+1)*29+i+1]]
|
||||
return zs[0]*(1-a)*(1-b)+zs[1]*a*(1-b)+zs[2]*(1-a)*b+zs[3]*a*b
|
||||
for n in range(950):
|
||||
x=random.uniform(-.77,.77);y=random.uniform(-1.32,1.32)
|
||||
if x>.43 and y<-.91:continue
|
||||
angle=random.random()*math.tau;size=random.uniform(.045,.105);start=len(verts)
|
||||
z=surface(x,y)+random.uniform(.012,.045)
|
||||
coords=[(0,0,.010)]
|
||||
for k in range(14):
|
||||
a=k*math.tau/14;r=1 if k%2==0 else .65
|
||||
coords.append((math.cos(a)*size*r,math.sin(a)*size*.55*r,random.uniform(-.004,.014)))
|
||||
lift=.23*math.exp(-((x/.50)**2+(y/.85)**2))
|
||||
for xx,yy,zz in coords:
|
||||
verts.append((x+xx*math.cos(angle)-yy*math.sin(angle),y+xx*math.sin(angle)+yy*math.cos(angle),z+zz));lifts.append(lift)
|
||||
idx=random.randrange(len(leafm))
|
||||
for k in range(14):faces.append((start,start+1+k,start+1+(k+1)%14));indices.append(idx)
|
||||
me=bpy.data.meshes.new('Curled oak leaf litter');me.from_pydata(verts,[],faces);me.update()
|
||||
leaves=bpy.data.objects.new('Leaves following breathing surface',me);s.collection.objects.link(leaves)
|
||||
for m in leafm:me.materials.append(m)
|
||||
for p,idx in zip(me.polygons,indices):p.material_index=idx
|
||||
leaves.shape_key_add(name='Rest');lk=leaves.shape_key_add(name='Breath')
|
||||
for v,lift in zip(lk.data,lifts):v.co.z+=lift
|
||||
for frame in range(1,481):
|
||||
s.frame_set(frame);lk.value=breath.value;lk.keyframe_insert('value',frame=frame)
|
||||
# Dimmer porch and blue moonlight, red glow beneath the grave.
|
||||
bpy.data.lights['Moon'].energy=220;bpy.data.lights['Porch'].energy=95;bpy.data.lights['Grave blue'].energy=95
|
||||
red=bpy.data.lights.new('Hidden red glow','POINT');red.energy=32;red.color=(1,.018,.004);red.shadow_soft_size=.28
|
||||
ob=bpy.data.objects.new('Hidden red glow',red);s.collection.objects.link(ob);ob.location=(0,-.3,.6)
|
||||
# Bare branches create recognisable long, eerie shadows.
|
||||
bark=material('Dark tree bark',(.025,.018,.014))
|
||||
def rod(name,a,b,r,m,parent=None):
|
||||
d=Vector(b)-Vector(a);bpy.ops.mesh.primitive_cylinder_add(vertices=10,radius=r,depth=d.length,location=(Vector(a)+Vector(b))/2);o=bpy.context.object;o.name=name;o.rotation_euler=d.to_track_quat('Z','Y').to_euler();o.data.materials.append(m)
|
||||
if parent:o.parent=parent
|
||||
return o
|
||||
for x,y in [(-3.3,2.5),(-4,-1.5)]:
|
||||
rod('Bare tree trunk',(x,y,0),(x+.1,y,3.8),.11,bark)
|
||||
for i in range(8):
|
||||
h=random.uniform(1.7,3.4);tip=(x+random.uniform(-1.5,1.5),y+random.uniform(-.8,.8),h+random.uniform(.3,1))
|
||||
rod('Bare branch',(x,y,h),tip,.035,bark)
|
||||
rod('Branch twig',tip,(tip[0]+.4,tip[1]-.2,tip[2]+.4),.012,bark)
|
||||
# Thin ground mist; faces remain readable.
|
||||
fog=bpy.data.materials.new('Thin ground mist');fog.use_nodes=True;nodes=fog.node_tree.nodes;nodes.clear();out=nodes.new('ShaderNodeOutputMaterial');vol=nodes.new('ShaderNodeVolumePrincipled');vol.inputs['Density'].default_value=.025;vol.inputs['Color'].default_value=(.45,.55,.65,1);fog.node_tree.links.new(vol.outputs['Volume'],out.inputs['Volume'])
|
||||
bpy.ops.mesh.primitive_cube_add(size=1,location=(0,0,.22));ob=bpy.context.object;ob.name='Low garden mist';ob.scale=(10,10,.40);ob.data.materials.append(fog)
|
||||
black=bpy.data.materials['Dark fabric']
|
||||
# Replace floating boots with jointed, stepping legs and swinging sleeves.
|
||||
roots=[bpy.data.objects[n] for n in ['Adult A','Adult B','Child witch','Child vampire']]
|
||||
for root,h in zip(roots,[1.78,1.68,1.05,1.2]):
|
||||
for ob in list(root.children):
|
||||
if ob.name.startswith('Boot'):bpy.data.objects.remove(ob,do_unlink=True)
|
||||
gate='((frame>=95 and frame<165) or (frame>=311 and frame<=344))'
|
||||
for side in [-1,1]:
|
||||
phase=0 if side<0 else math.pi
|
||||
hip=bpy.data.objects.new(root.name+' leg pivot '+str(side),None);s.collection.objects.link(hip);hip.parent=root;hip.location=(side*h*.075,0,h*.40)
|
||||
upper=rod('Trouser thigh',(0,0,0),(0,0,-h*.17),h*.041,black,hip)
|
||||
knee=bpy.data.objects.new(root.name+' knee '+str(side),None);s.collection.objects.link(knee);knee.parent=hip;knee.location=(0,0,-h*.17)
|
||||
rod('Trouser shin',(0,0,0),(0,0,-h*.16),h*.036,black,knee)
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=16,ring_count=8,location=(0,.025,-h*.19));boot=bpy.context.object;boot.name='Animated boot';boot.parent=knee;boot.scale=(h*.055,h*.10,h*.045);boot.data.materials.append(black)
|
||||
hip.driver_add('rotation_euler',0).driver.expression=f'0.40*sin(frame*0.34+{phase}) if {gate} else 0'
|
||||
knee.driver_add('rotation_euler',0).driver.expression=f'-0.40*max(0,-sin(frame*0.34+{phase})) if {gate} else 0'
|
||||
for idx,ob in enumerate([o for o in root.children if o.name.startswith('Sleeve')]):
|
||||
ob.driver_add('rotation_euler',0).driver.expression=f'0.20*sin(frame*0.34+{idx*math.pi}) if {gate} else 0.1'
|
||||
# Keep the family at the grave for the fictional fright scene.
|
||||
root.animation_data_clear()
|
||||
child=root.name.startswith('Child');idx=roots.index(root)
|
||||
x=[3.25,3.9,2.10,2.82][idx];y=[.05,.8,-.55,-1.0][idx]
|
||||
positions=[(1,(x,-9-idx*.35,0)),(95,(x,-5-idx*.25,0)),(165,(x,y,0)),(310,(x,y,0)),(325,(x+.20,y-.30,0)),(345,(x+.22,y-.40,0)),(480,(x+.22,y-.4,0))]
|
||||
for f,pos in positions:root.location=(pos[0],pos[1],pos[2]+.065);root.keyframe_insert('location',frame=f)
|
||||
for f,ang in [(1,0),(165,-.15),(310,-.15),(321,.18 if child else .07),(345,0),(480,0)]:root.rotation_euler.z=ang;root.keyframe_insert('rotation_euler',frame=f)
|
||||
root.driver_add('rotation_euler',0).driver.expression='-0.14 if 316<=frame<=328 else 0'
|
||||
# Camera remains with the family; no later jump to an adults-only sequence.
|
||||
cam=s.camera;cam.animation_data_clear();cam.data.lens=40
|
||||
for f,loc,target in [(1,(7.5,-9,4),(.7,0,.6)),(165,(6.8,-7.6,3.3),(.7,0,.65)),(310,(6.8,-7.6,3.3),(.7,0,.65)),(320,(6.6,-7.5,3.15),(.7,0,.65)),(480,(7.5,-9,4),(.7,0,.6))]:
|
||||
cam.location=loc;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)
|
||||
# Warm reveal is brief and darker than the previous white flash.
|
||||
flash=bpy.data.lights['Brief reveal'];flash.animation_data_clear();flash.color=(1,.24,.07)
|
||||
for f,power in [(1,0),(310,0),(316,350),(322,65),(332,140),(340,0),(480,0)]:flash.energy=power;flash.keyframe_insert('energy',frame=f)
|
||||
s.frame_set(200);s.render.image_settings.file_format='PNG';s.render.filepath=os.path.join(OUT,'example_')
|
||||
bpy.ops.file.pack_all();bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_examples.blend'))
|
||||
for frame in [140,200,320]:
|
||||
s.frame_set(frame);s.render.filepath=os.path.join(OUT,'example_%03d.png'%frame);bpy.ops.render.render(write_still=True)
|
||||
print('V2 EXAMPLES COMPLETE — no video rendered')
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Improve the existing concept scene; render still examples only."""
|
||||
import bpy, math, random, os
|
||||
from mathutils import Vector
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview','v3');os.makedirs(OUT,exist_ok=True)
|
||||
bpy.ops.wm.open_mainfile(filepath=os.path.join(ROOT,'preview','halloween_preview.blend'))
|
||||
s=bpy.context.scene;s.eevee.taa_render_samples=32
|
||||
random.seed(104)
|
||||
def material(name,c,rough=.8):
|
||||
m=bpy.data.materials.new(name);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
|
||||
return m
|
||||
leafm=[material('Dry oak leaves '+str(i),c) for i,c in enumerate([(.20,.055,.012),(.32,.14,.032),(.10,.042,.013),(.40,.23,.075),(.16,.08,.025)])]
|
||||
shroud=bpy.data.objects['Breathing burial shroud'];base=shroud.data.shape_keys.key_blocks['Rest'];breath=shroud.data.shape_keys.key_blocks['Breath']
|
||||
# A real opening at the hand's emergence point, rather than intersecting cloth.
|
||||
import bmesh
|
||||
bm=bmesh.new();bm.from_mesh(shroud.data)
|
||||
for face in list(bm.faces):
|
||||
c=face.calc_center_median()
|
||||
if abs(c.x)<.16 and abs(c.y)<.18:bm.faces.remove(face)
|
||||
bm.to_mesh(shroud.data);bm.free()
|
||||
# Leaf litter follows the animated cloth with its own synchronized shape key.
|
||||
verts=[];faces=[];indices=[];lifts=[]
|
||||
def surface(x,y):
|
||||
u=(x+.8)/1.6*28;v=(y+1.35)/2.7*44;i=min(27,max(0,int(u)));j=min(43,max(0,int(v)));a=u-i;b=v-j
|
||||
zs=[base.data[k].co.z for k in [j*29+i,j*29+i+1,(j+1)*29+i,(j+1)*29+i+1]]
|
||||
return zs[0]*(1-a)*(1-b)+zs[1]*a*(1-b)+zs[2]*(1-a)*b+zs[3]*a*b
|
||||
for n in range(950):
|
||||
x=random.uniform(-.77,.77);y=random.uniform(-1.32,1.32)
|
||||
if x*x+y*y<.24**2:continue
|
||||
angle=random.random()*math.tau;size=random.uniform(.045,.105);start=len(verts)
|
||||
z=surface(x,y)+random.uniform(.012,.045)
|
||||
coords=[(0,0,.010)]
|
||||
for k in range(14):
|
||||
a=k*math.tau/14;r=1 if k%2==0 else .65
|
||||
coords.append((math.cos(a)*size*r,math.sin(a)*size*.55*r,random.uniform(-.004,.014)))
|
||||
lift=.23*math.exp(-((x/.50)**2+(y/.85)**2))
|
||||
for xx,yy,zz in coords:
|
||||
verts.append((x+xx*math.cos(angle)-yy*math.sin(angle),y+xx*math.sin(angle)+yy*math.cos(angle),z+zz));lifts.append(lift)
|
||||
idx=random.randrange(len(leafm))
|
||||
for k in range(14):faces.append((start,start+1+k,start+1+(k+1)%14));indices.append(idx)
|
||||
me=bpy.data.meshes.new('Curled oak leaf litter');me.from_pydata(verts,[],faces);me.update()
|
||||
leaves=bpy.data.objects.new('Leaves following breathing surface',me);s.collection.objects.link(leaves)
|
||||
for m in leafm:me.materials.append(m)
|
||||
for p,idx in zip(me.polygons,indices):p.material_index=idx
|
||||
leaves.shape_key_add(name='Rest');lk=leaves.shape_key_add(name='Breath')
|
||||
for v,lift in zip(lk.data,lifts):v.co.z+=lift
|
||||
for frame in range(1,481):
|
||||
s.frame_set(frame);lk.value=breath.value;lk.keyframe_insert('value',frame=frame)
|
||||
# A layer covering the middle opening bursts upward with the emerging hand.
|
||||
for n in range(110):
|
||||
angle=random.uniform(0,math.tau);radius=.23*math.sqrt(random.random())
|
||||
x=radius*math.cos(angle);y=radius*math.sin(angle);z0=surface(x,y)+.035
|
||||
size=random.uniform(.055,.10);vs=[(0,0,.009)]
|
||||
for k in range(14):
|
||||
theta=k*math.tau/14;r=1 if k%2==0 else .65
|
||||
vs.append((size*r*math.cos(theta),size*.55*r*math.sin(theta),random.uniform(-.004,.012)))
|
||||
me=bpy.data.meshes.new('Flying curled leaf');me.from_pydata(vs,[],[(0,k+1,(k+1)%14+1) for k in range(14)]);me.update()
|
||||
obj=bpy.data.objects.new('Burst leaf %03d'%n,me);s.collection.objects.link(obj);obj.data.materials.append(random.choice(leafm))
|
||||
delay=random.uniform(0,.11);speed=random.uniform(.35,.85);up=random.uniform(2.0,3.0);spin=random.uniform(-7,7);initial=random.uniform(0,math.tau)
|
||||
for frame in [1,200,310]+list(range(311,371))+[480]:
|
||||
s.frame_set(frame);t=max(0,(frame-313)/24-delay)
|
||||
flight=min(t,2*up/5.4);direction=angle+.8*flight
|
||||
xx=x+speed*flight*math.cos(direction);yy=y+speed*flight*math.sin(direction)
|
||||
zz=z0+up*flight-2.7*flight*flight
|
||||
if t==0:zz+=.23*math.exp(-((x/.50)**2+(y/.85)**2))*breath.value
|
||||
else:zz=max(.025,zz)
|
||||
obj.location=(xx,yy,zz);obj.rotation_euler=(spin*flight,.7*spin*flight,initial+spin*flight)
|
||||
obj.keyframe_insert('location',frame=frame);obj.keyframe_insert('rotation_euler',frame=frame)
|
||||
for fc in obj.animation_data.action.fcurves:
|
||||
for key in fc.keyframe_points:key.interpolation='LINEAR'
|
||||
hand=bpy.data.objects['Emerging hand'];hand.animation_data_clear();hand.location=(0,0,-.65)
|
||||
for frame,z in [(1,-.65),(310,-.65),(314,-.35),(322,.15),(340,.15),(365,-.65),(480,-.65)]:
|
||||
hand.location.z=z;hand.keyframe_insert('location',frame=frame)
|
||||
|
||||
# Dimmer porch and blue moonlight, red glow beneath the grave.
|
||||
bpy.data.lights['Moon'].energy=220;bpy.data.lights['Porch'].energy=95;bpy.data.lights['Grave blue'].energy=95
|
||||
red=bpy.data.lights.new('Hidden red glow','POINT');red.energy=32;red.color=(1,.018,.004);red.shadow_soft_size=.28
|
||||
ob=bpy.data.objects.new('Hidden red glow',red);s.collection.objects.link(ob);ob.location=(0,-.3,.6)
|
||||
# Bare branches create recognisable long, eerie shadows.
|
||||
bark=material('Dark tree bark',(.025,.018,.014))
|
||||
def rod(name,a,b,r,m,parent=None):
|
||||
d=Vector(b)-Vector(a);bpy.ops.mesh.primitive_cylinder_add(vertices=10,radius=r,depth=d.length,location=(Vector(a)+Vector(b))/2);o=bpy.context.object;o.name=name;o.rotation_euler=d.to_track_quat('Z','Y').to_euler();o.data.materials.append(m)
|
||||
if parent:o.parent=parent
|
||||
return o
|
||||
for x,y in [(-3.3,2.5),(-4,-1.5)]:
|
||||
rod('Bare tree trunk',(x,y,0),(x+.1,y,3.8),.11,bark)
|
||||
for i in range(8):
|
||||
h=random.uniform(1.7,3.4);tip=(x+random.uniform(-1.5,1.5),y+random.uniform(-.8,.8),h+random.uniform(.3,1))
|
||||
rod('Bare branch',(x,y,h),tip,.035,bark)
|
||||
rod('Branch twig',tip,(tip[0]+.4,tip[1]-.2,tip[2]+.4),.012,bark)
|
||||
# Thin ground mist; faces remain readable.
|
||||
fog=bpy.data.materials.new('Thin ground mist');fog.use_nodes=True;nodes=fog.node_tree.nodes;nodes.clear();out=nodes.new('ShaderNodeOutputMaterial');vol=nodes.new('ShaderNodeVolumePrincipled');vol.inputs['Density'].default_value=.025;vol.inputs['Color'].default_value=(.45,.55,.65,1);fog.node_tree.links.new(vol.outputs['Volume'],out.inputs['Volume'])
|
||||
bpy.ops.mesh.primitive_cube_add(size=1,location=(0,0,.22));ob=bpy.context.object;ob.name='Low garden mist';ob.scale=(10,10,.40);ob.data.materials.append(fog)
|
||||
black=bpy.data.materials['Dark fabric']
|
||||
# Replace floating boots with jointed, stepping legs and swinging sleeves.
|
||||
roots=[bpy.data.objects[n] for n in ['Adult A','Adult B','Child witch','Child vampire']]
|
||||
for root,h in zip(roots,[1.78,1.68,1.05,1.2]):
|
||||
for ob in list(root.children):
|
||||
if ob.name.startswith('Boot'):bpy.data.objects.remove(ob,do_unlink=True)
|
||||
gate='((frame>=95 and frame<165) or (frame>=311 and frame<=344))'
|
||||
for side in [-1,1]:
|
||||
phase=0 if side<0 else math.pi
|
||||
hip=bpy.data.objects.new(root.name+' leg pivot '+str(side),None);s.collection.objects.link(hip);hip.parent=root;hip.location=(side*h*.075,0,h*.40)
|
||||
upper=rod('Trouser thigh',(0,0,0),(0,0,-h*.17),h*.041,black,hip)
|
||||
knee=bpy.data.objects.new(root.name+' knee '+str(side),None);s.collection.objects.link(knee);knee.parent=hip;knee.location=(0,0,-h*.17)
|
||||
rod('Trouser shin',(0,0,0),(0,0,-h*.16),h*.036,black,knee)
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=16,ring_count=8,location=(0,.025,-h*.19));boot=bpy.context.object;boot.name='Animated boot';boot.parent=knee;boot.scale=(h*.055,h*.10,h*.045);boot.data.materials.append(black)
|
||||
hip.driver_add('rotation_euler',0).driver.expression=f'0.40*sin(frame*0.34+{phase}) if {gate} else 0'
|
||||
knee.driver_add('rotation_euler',0).driver.expression=f'-0.40*max(0,-sin(frame*0.34+{phase})) if {gate} else 0'
|
||||
for idx,ob in enumerate([o for o in root.children if o.name.startswith('Sleeve')]):
|
||||
ob.driver_add('rotation_euler',0).driver.expression=f'0.20*sin(frame*0.34+{idx*math.pi}) if {gate} else 0.1'
|
||||
# Keep the family at the grave for the fictional fright scene.
|
||||
root.animation_data_clear()
|
||||
child=root.name.startswith('Child');idx=roots.index(root)
|
||||
x=[3.25,3.9,2.10,2.82][idx];y=[.05,.8,-.55,-1.0][idx]
|
||||
positions=[(1,(x,-9-idx*.35,0)),(95,(x,-5-idx*.25,0)),(165,(x,y,0)),(310,(x,y,0)),(325,(x+.20,y-.30,0)),(345,(x+.22,y-.40,0)),(480,(x+.22,y-.4,0))]
|
||||
for f,pos in positions:root.location=(pos[0],pos[1],pos[2]+.065);root.keyframe_insert('location',frame=f)
|
||||
for f,ang in [(1,0),(165,-.15),(310,-.15),(321,.18 if child else .07),(345,0),(480,0)]:root.rotation_euler.z=ang;root.keyframe_insert('rotation_euler',frame=f)
|
||||
root.driver_add('rotation_euler',0).driver.expression='-0.14 if 316<=frame<=328 else 0'
|
||||
# Camera remains with the family; no later jump to an adults-only sequence.
|
||||
cam=s.camera;cam.animation_data_clear();cam.data.lens=40
|
||||
for f,loc,target in [(1,(7.5,-9,4),(.7,0,.6)),(165,(6.8,-7.6,3.3),(.7,0,.65)),(310,(6.8,-7.6,3.3),(.7,0,.65)),(320,(6.6,-7.5,3.15),(.7,0,.65)),(480,(7.5,-9,4),(.7,0,.6))]:
|
||||
cam.location=loc;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)
|
||||
# Warm reveal is brief and darker than the previous white flash.
|
||||
flash=bpy.data.lights['Brief reveal'];flash.animation_data_clear();flash.color=(1,.24,.07)
|
||||
for f,power in [(1,0),(310,0),(316,350),(322,65),(332,140),(340,0),(480,0)]:flash.energy=power;flash.keyframe_insert('energy',frame=f)
|
||||
s.frame_set(200);s.render.image_settings.file_format='PNG';s.render.filepath=os.path.join(OUT,'example_')
|
||||
bpy.ops.file.pack_all();bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_examples.blend'))
|
||||
for frame in [310,322,334]:
|
||||
s.frame_set(frame);s.render.filepath=os.path.join(OUT,'example_%03d.png'%frame);bpy.ops.render.render(write_still=True)
|
||||
print('V3 EXAMPLES COMPLETE — no video rendered')
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Darker night lighting with visible practical sources; still renders only."""
|
||||
import bpy, os, math
|
||||
from mathutils import Vector
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview','v4');os.makedirs(OUT,exist_ok=True)
|
||||
bpy.ops.wm.open_mainfile(filepath=os.path.join(ROOT,'preview','v3','halloween_examples.blend'))
|
||||
s=bpy.context.scene;s.render.engine='BLENDER_EEVEE';s.eevee.taa_render_samples=64
|
||||
s.eevee.use_gtao=True;s.eevee.gtao_distance=2;s.eevee.use_ssr=True;s.eevee.use_ssr_refraction=True
|
||||
s.eevee.use_soft_shadows=True;s.eevee.shadow_cube_size='2048';s.eevee.use_bloom=True;s.eevee.bloom_intensity=.045;s.eevee.bloom_threshold=1.2
|
||||
s.view_settings.exposure=-.6
|
||||
s.world.use_nodes=True;bg=s.world.node_tree.nodes.get('Background');bg.inputs['Color'].default_value=(.002,.004,.008,1);bg.inputs['Strength'].default_value=.15
|
||||
for l in bpy.data.lights:
|
||||
l.use_shadow=True;l.use_contact_shadow=True;l.contact_shadow_distance=.4;l.contact_shadow_bias=.015
|
||||
moon=bpy.data.lights['Moon'];moon.energy=95;moon.color=(.55,.65,1);moon.size=5
|
||||
bpy.data.lights['Porch'].energy=0
|
||||
blue=bpy.data.lights['Grave blue'];blue.energy=30;blue.color=(.12,.24,1);blue.size=.7
|
||||
red=bpy.data.lights['Hidden red glow'];red.energy=5;red.color=(1,.055,.008);red.shadow_soft_size=.12
|
||||
bpy.data.objects['Hidden red glow'].location=(-.72,.7,.31)
|
||||
# Light actually emanates from the lit windows, casting warm patches on the path.
|
||||
for x in [-3,0,3]:
|
||||
l=bpy.data.lights.new('Window spill '+str(x),'AREA');l.energy=24;l.color=(1,.59,.28);l.shape='RECTANGLE';l.size=1.3;l.size_y=1.5
|
||||
l.use_contact_shadow=True
|
||||
o=bpy.data.objects.new(l.name,l);s.collection.objects.link(o);o.location=(x,5.55,2.2);o.rotation_euler=(Vector((x,1,.2))-o.location).to_track_quat('-Z','Y').to_euler()
|
||||
m=bpy.data.materials['Warm window '+str(x)];p=m.node_tree.nodes.get('Principled BSDF');p.inputs['Emission Color'].default_value=(1,.52,.19,1);p.inputs['Emission Strength'].default_value=1.5
|
||||
# Small visible porch lantern with warm light and subtle irregular flicker.
|
||||
em=bpy.data.materials.new('Lantern glowing glass');em.use_nodes=True;p=em.node_tree.nodes.get('Principled BSDF');p.inputs['Base Color'].default_value=(.6,.18,.02,1);p.inputs['Emission Color'].default_value=(1,.45,.12,1);p.inputs['Emission Strength'].default_value=4
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=16,ring_count=8,location=(2,5.35,2.15));o=bpy.context.object;o.name='Porch lantern glass';o.scale=(.065,.065,.12);o.data.materials.append(em)
|
||||
for z in [2.02,2.28]:
|
||||
bpy.ops.mesh.primitive_cube_add(size=1,location=(2,5.35,z));o=bpy.context.object;o.name='Lantern metal cap';o.scale=(.19,.16,.035);o.data.materials.append(bpy.data.materials['Dark fabric'])
|
||||
l=bpy.data.lights.new('Porch lantern light','POINT');l.color=(1,.56,.23);l.shadow_soft_size=.09;l.use_contact_shadow=True
|
||||
ob=bpy.data.objects.new(l.name,l);s.collection.objects.link(ob);ob.location=(2,5.20,2.15)
|
||||
for frame in range(1,481,3):l.energy=17+1.8*math.sin(frame*.17)+.8*math.sin(frame*.71);l.keyframe_insert('energy',frame=frame)
|
||||
# Damp pavers catch localized highlights; leave soil and cloth matte.
|
||||
p=bpy.data.materials['Weathered limestone'].node_tree.nodes.get('Principled BSDF');p.inputs['Roughness'].default_value=.48
|
||||
# Directed flash briefly reveals hand and airborne leaves, rather than lighting the whole garden.
|
||||
ob=bpy.data.objects['Brief reveal'];old=ob.data;flash=bpy.data.lights.new('Directed grave reveal','SPOT');ob.data=flash;flash.use_contact_shadow=True;flash.spot_size=math.radians(65);flash.spot_blend=.65;flash.shadow_soft_size=.13;flash.color=(1,.67,.36)
|
||||
for frame,power in [(1,0),(310,0),(316,700),(320,450),(324,180),(332,380),(340,0),(480,0)]:flash.energy=power;flash.keyframe_insert('energy',frame=frame)
|
||||
# Hint of light scattering in the low mist, without a luminous fog blanket.
|
||||
vol=bpy.data.materials['Thin ground mist'].node_tree.nodes.get('Principled Volume');vol.inputs['Density'].default_value=.035;vol.inputs['Anisotropy'].default_value=.25
|
||||
s.frame_set(310);s.render.image_settings.file_format='PNG';s.render.filepath=os.path.join(OUT,'example_')
|
||||
bpy.ops.file.pack_all();bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_examples.blend'))
|
||||
for frame in [310,322,334]:
|
||||
s.frame_set(frame);s.render.filepath=os.path.join(OUT,'example_%03d.png'%frame);bpy.ops.render.render(write_still=True)
|
||||
print('DARKER EXAMPLES COMPLETE — still images only')
|
||||
@@ -0,0 +1,175 @@
|
||||
import bpy, math, random, os, sys, wave, struct
|
||||
from mathutils import Vector
|
||||
random.seed(18)
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview')
|
||||
bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete(use_global=False)
|
||||
s=bpy.context.scene
|
||||
s.render.engine='BLENDER_EEVEE'; s.eevee.taa_render_samples=8; s.eevee.use_gtao=True; s.eevee.gtao_distance=3; s.eevee.use_soft_shadows=True
|
||||
s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100
|
||||
s.render.fps=24;s.frame_start=1;s.frame_end=480
|
||||
s.world.color=(.003,.003,.003)
|
||||
s.view_settings.view_transform='AgX'
|
||||
def mat(name,color,rough=.7,noise=False):
|
||||
m=bpy.data.materials.new(name);m.diffuse_color=(*color,1);m.use_nodes=True
|
||||
p=m.node_tree.nodes.get('Principled BSDF');p.inputs['Base Color'].default_value=(*color,1);p.inputs['Roughness'].default_value=rough
|
||||
if noise:
|
||||
n=m.node_tree.nodes.new('ShaderNodeTexNoise');n.inputs['Scale'].default_value=24
|
||||
b=m.node_tree.nodes.new('ShaderNodeBump');b.inputs['Strength'].default_value=.3;b.inputs['Distance'].default_value=.08
|
||||
m.node_tree.links.new(n.outputs['Fac'],b.inputs['Height']);m.node_tree.links.new(b.outputs['Normal'],p.inputs['Normal'])
|
||||
return m
|
||||
soil=mat('Damp garden soil',(.07,.045,.025),noise=True);stone=mat('Weathered limestone',(.28,.29,.27),noise=True)
|
||||
cloth=mat('Burial cloth',(.15,.12,.09),noise=True);brick=mat('Rendered house wall',(.24,.19,.14),noise=True)
|
||||
bone=mat('Old ivory',(.55,.48,.32));black=mat('Dark fabric',(.015,.02,.025),noise=True)
|
||||
leafm=[mat('Autumn leaf '+str(i),c) for i,c in enumerate([(.21,.065,.014),(.30,.15,.03),(.12,.055,.018)])]
|
||||
def cube(name,loc,scale,m,bevel=0):
|
||||
bpy.ops.mesh.primitive_cube_add(size=1,location=loc);o=bpy.context.object;o.name=name;o.scale=scale
|
||||
bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
|
||||
if m:o.data.materials.append(m)
|
||||
if bevel:
|
||||
mod=o.modifiers.new('Soft edges','BEVEL');mod.width=bevel;mod.segments=3;o.data.use_auto_smooth=True;o.modifiers.new('Normals','WEIGHTED_NORMAL')
|
||||
return o
|
||||
def uv(name,loc,scale,m):
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=16,ring_count=10,location=loc);o=bpy.context.object;o.name=name;o.scale=scale;o.data.materials.append(m)
|
||||
for p in o.data.polygons:p.use_smooth=True
|
||||
return o
|
||||
def rod(name,a,b,r,m,parent=None):
|
||||
d=Vector(b)-Vector(a);bpy.ops.mesh.primitive_cylinder_add(vertices=10,radius=r,depth=d.length,location=(Vector(a)+Vector(b))/2)
|
||||
o=bpy.context.object;o.name=name;o.rotation_euler=d.to_track_quat('Z','Y').to_euler();o.data.materials.append(m)
|
||||
if parent:o.parent=parent
|
||||
return o
|
||||
cube('Ground',(0,0,-.12),(16,16,.2),soil)
|
||||
cube('Walkway',(2.6,0,.01),(1.5,12,.09),stone,.03)
|
||||
for y in range(-6,7):cube('Paving joint',(2.6,y,.065),(1.5,.017,.005),black)
|
||||
cube('House',(0,6,2),(12,.3,4),brick)
|
||||
for x in [-3,0,3]:
|
||||
cube('Window frame',(x,5.80,2.2),(1.6,.15,1.8),black,.04)
|
||||
glass=mat('Warm window '+str(x),(.5,.22,.045));p=glass.node_tree.nodes.get('Principled BSDF');p.inputs['Emission Color'].default_value=(1,.35,.08,1);p.inputs['Emission Strength'].default_value=.8
|
||||
cube('Lit window',(x,5.70,2.2),(1.4,.04,1.6),glass)
|
||||
cube('Window divider',(x,5.65,2.2),(.05,.06,1.7),black)
|
||||
for x in [-5,5]:
|
||||
for y in range(-4,6):cube('Fence post',(x,y,.65),(.09,.09,1.3),stone,.015)
|
||||
cube('Fence rail',(x,.5,.9),(.07,10,.08),stone)
|
||||
# Grave border and headstone
|
||||
for x in [-.9,.9]:cube('Grave side',(x,0,.14),(.15,2.9,.28),stone,.04)
|
||||
for y in [-1.45,1.45]:cube('Grave end',(0,y,.14),(1.95,.15,.28),stone,.04)
|
||||
cube('Headstone base',(0,1.65,.25),(1.1,.45,.3),stone,.06)
|
||||
cube('Headstone',(0,1.65,.85),(.85,.22,1.1),stone,.10)
|
||||
uv('Rounded headstone',(0,1.65,1.35),(.425,.11,.3),stone)
|
||||
def text(body,loc,size):
|
||||
bpy.ops.object.text_add(location=loc,rotation=(math.pi/2,0,0));o=bpy.context.object;o.data.body=body;o.data.align_x='CENTER';o.data.size=size;o.data.extrude=.002;o.data.materials.append(black)
|
||||
text('R I P',(0,1.525,1.05),.19);text('STILL BREATHING',(0,1.525,.72),.065)
|
||||
# Fabric mesh with animated breathing shape
|
||||
verts=[];faces=[];nx=28;ny=44
|
||||
for j in range(ny+1):
|
||||
y=-1.35+2.7*j/ny
|
||||
for i in range(nx+1):
|
||||
x=-.80+1.6*i/nx;z=.19+.035*math.sin(i*1.3+j*.7)*math.sin(math.pi*i/nx)*math.sin(math.pi*j/ny);verts.append((x,y,z))
|
||||
for j in range(ny):
|
||||
for i in range(nx):
|
||||
a=j*(nx+1)+i;faces.append((a,a+1,a+nx+2,a+nx+1))
|
||||
mesh=bpy.data.meshes.new('Cloth grid');mesh.from_pydata(verts,[],faces);mesh.update()
|
||||
o=bpy.data.objects.new('Breathing burial shroud',mesh);s.collection.objects.link(o);o.data.materials.append(cloth)
|
||||
for p in mesh.polygons:p.use_smooth=True
|
||||
o.shape_key_add(name='Rest');breath=o.shape_key_add(name='Breath')
|
||||
for v in breath.data:v.co.z+=.23*math.exp(-((v.co.x/.50)**2+(v.co.y/.85)**2))
|
||||
for f,val in [(1,.1),(30,1),(65,0),(100,.85),(120,.15),(150,0),(175,.45),(210,0),(240,.1),(241,0),(310,0),(316,1.8),(322,.1),(332,1.2),(345,0),(380,.1),(415,.8),(450,0),(480,.35)]:breath.value=val;breath.keyframe_insert('value',frame=f)
|
||||
for i in range(220):
|
||||
x=random.uniform(-4,4);y=random.uniform(-4,4)
|
||||
if abs(x)<1 and abs(y)<1.5:continue
|
||||
me=bpy.data.meshes.new('Leaf');me.from_pydata([(-.10,0,0),(-.045,-.04,0),(0,-.065,.01),(.07,-.025,0),(.11,0,0),(.04,.045,0),(-.02,.055,.008)],[],[(0,1,2,3,4,5,6)]);me.update()
|
||||
l=bpy.data.objects.new('Fallen leaf',me);s.collection.objects.link(l);l.location=(x,y,.025);l.data.materials.append(random.choice(leafm));l.rotation_euler.z=random.random()*6.28
|
||||
# Grass tufts, shrubs, and Halloween pumpkins
|
||||
grassmat=mat('Dry grass',(.055,.065,.024))
|
||||
for i in range(180):
|
||||
x=random.uniform(-4.5,1.6);y=random.uniform(-4,4)
|
||||
if abs(x)<1.1 and abs(y)<1.8:continue
|
||||
rod('Grass blade',(x,y,0),(x+.025,y+.015,random.uniform(.07,.17)),.003,grassmat)
|
||||
for x,y in [(-2,1),(-3,3),(4,4)]:
|
||||
for j in range(8):uv('Garden shrub',(x+random.uniform(-.3,.3),y+random.uniform(-.3,.3),random.uniform(.2,.5)),(.3,.3,.3),grassmat)
|
||||
for x,y in [(-1.5,-1.7),(-1.8,-1.4),(.8,2.1)]:
|
||||
pumpkin=uv('Ribbed pumpkin',(x,y,.18),(.23,.23,.18),leafm[0])
|
||||
for v in pumpkin.data.vertices:
|
||||
ang=math.atan2(v.co.y,v.co.x);fac=1+.055*math.cos(ang*10);v.co.x*=fac;v.co.y*=fac
|
||||
rod('Pumpkin stem',(x,y,.32),(x+.02,y,.42),.024,soil)
|
||||
# Cloaked costumed visitors, seen from behind/side
|
||||
purple=mat('Purple wool costume',(.11,.025,.13),noise=True);red=mat('Burgundy costume',(.18,.018,.022),noise=True)
|
||||
def person(name,h,color,start,end,child=False):
|
||||
root=bpy.data.objects.new(name,None);s.collection.objects.link(root)
|
||||
# Folded tapered cloak
|
||||
vs=[];fs=[];rings=14;seg=32
|
||||
for j in range(rings):
|
||||
z=h*(.18+.60*j/(rings-1));r=h*(.24-.12*j/(rings-1))
|
||||
for k in range(seg):
|
||||
ang=k*2*math.pi/seg;rr=r*(1+.085*math.sin(ang*9+j*.25));vs.append((rr*math.cos(ang),rr*.65*math.sin(ang),z))
|
||||
for j in range(rings-1):
|
||||
for k in range(seg):a=j*seg+k;b=j*seg+(k+1)%seg;fs.append((a,b,b+seg,a+seg))
|
||||
me=bpy.data.meshes.new(name+' costume');me.from_pydata(vs,[],fs);me.update();ob=bpy.data.objects.new(name+' cloak',me);s.collection.objects.link(ob);ob.parent=root;ob.data.materials.append(color)
|
||||
for p in me.polygons:p.use_smooth=True
|
||||
for x in [-h*.08,h*.08]:
|
||||
ob=uv('Boot',(x,-.04,h*.08),(h*.065,h*.10,h*.09),black);ob.parent=root
|
||||
drv=ob.driver_add('location',2).driver;drv.expression=f'{h*.08} + (max(0,sin(frame*0.32+{0 if x<0 else math.pi}))*{h*.035} if 120<frame<165 else 0)'
|
||||
ob=uv('Hood',(0,0,h*.86),(h*.12,h*.12,h*.15),color);ob.parent=root
|
||||
# dark face opening directed toward grave (-X)
|
||||
ob=uv('Hood shadow',(-h*.10,0,h*.85),(h*.025,h*.075,h*.09),black);ob.parent=root
|
||||
for sign in [-1,1]:
|
||||
ob=uv('Sleeve',(0,sign*h*.14,h*.56),(h*.07,h*.07,h*.24),color);ob.rotation_euler.x=sign*.12;ob.parent=root
|
||||
if child:
|
||||
ob=uv('Pumpkin candy bucket',(-h*.12,-h*.27,h*.35),(h*.09,h*.09,h*.08),leafm[1]);ob.parent=root
|
||||
for f,pos in start:root.location=pos;root.keyframe_insert('location',frame=f)
|
||||
for f,angle in end:root.rotation_euler.z=angle;root.keyframe_insert('rotation_euler',frame=f)
|
||||
return root
|
||||
person('Adult A',1.78,black,[(1,(3.2,-10,0)),(120,(3.2,-3,0)),(165,(2.7,.0,0)),(235,(2.7,1.5,0)),(240,(3,7,0)),(241,(2.7,-.35,0)),(310,(2.7,-.35,0)),(325,(3.05,-.75,0)),(390,(3.05,-.75,0)),(480,(3.2,.2,0))],[(1,0),(310,0),(325,-.15),(380,0)])
|
||||
person('Adult B',1.68,red,[(1,(3.6,-10.8,0)),(120,(3.6,-3.6,0)),(165,(3.35,-.5,0)),(235,(3.35,1.0,0)),(240,(3,7,0)),(241,(3.45,.5,0)),(310,(3.45,.5,0)),(325,(3.75,.9,0)),(480,(3.65,1.3,0))],[(1,0),(310,0),(325,.15),(380,0)])
|
||||
for name,h,x,y,c in [('Child witch',1.05,2.35,-.8,purple),('Child vampire',1.2,3.1,-1.35,black)]:
|
||||
person(name,h,c,[(1,(x,-11,0)),(120,(x,-3.8,0)),(165,(x,y,0)),(220,(x,y+.8,0)),(239,(x,3.5,0)),(240,(x,8,0)),(480,(x,8,0))],[(1,0)],True)
|
||||
# Emerging skeletal hand
|
||||
hand=bpy.data.objects.new('Emerging hand',None);s.collection.objects.link(hand);hand.location=(.65,-1.15,-.4)
|
||||
ob=uv('Palm',(0,0,.4),(.09,.045,.12),bone);ob.parent=hand
|
||||
rod('Forearm',(0,0,0),(0,0,.32),.045,bone,hand)
|
||||
for i in range(4):
|
||||
x=(i-1.5)*.045;top=.64+(.025 if i in [1,2] else 0)
|
||||
rod('Finger',(x,0,.48),(x,-.025,top),.015,bone,hand);rod('Finger tip',(x,-.025,top),(x,-.065,top+.035),.012,bone,hand)
|
||||
rod('Thumb',(-.075,0,.4),(-.14,-.04,.48),.02,bone,hand)
|
||||
for f,z in [(1,-.65),(310,-.65),(320,.03),(345,.05),(365,-.65),(480,-.65)]:hand.location.z=z;hand.keyframe_insert('location',frame=f)
|
||||
def light(name,loc,color,power,size=3):
|
||||
data=bpy.data.lights.new(name,'AREA');data.energy=power;data.color=color;data.shape='DISK';data.size=size
|
||||
ob=bpy.data.objects.new(name,data);s.collection.objects.link(ob);ob.location=loc;ob.rotation_euler=(Vector((0,0,.3))-ob.location).to_track_quat('-Z','Y').to_euler();return data
|
||||
light('Moon',(0,-2,7),(.27,.46,1),450,7)
|
||||
light('Porch',(1,4,3),(1,.46,.16),240,4)
|
||||
light('Grave blue',(-1,1,1.9),(.025,.12,1),110,2)
|
||||
flash=light('Brief reveal',(1,-2,2),(1,.72,.38),0,1)
|
||||
for f,e in [(1,0),(312,0),(316,600),(321,40),(330,260),(337,0),(480,0)]:flash.energy=e;flash.keyframe_insert('energy',frame=f)
|
||||
# Cinematic camera
|
||||
bpy.ops.object.camera_add();cam=bpy.context.object;s.camera=cam;cam.data.lens=38
|
||||
for f,loc,target in [(1,(7,-9,4),(0,0,.55)),(120,(4.8,-6.3,3.1),(0,0,.55)),(240,(4.8,-6.3,3.1),(.7,0,.65)),(241,(6.5,-7,3.3),(.6,0,.65)),(380,(6.5,-7,3.3),(.6,0,.65)),(480,(7,-8.5,4.5),(0,.5,.6))]:
|
||||
cam.location=loc;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)
|
||||
# Make cuts and visitor reset instantaneous rather than interpolated
|
||||
for ob in [cam]+[o for o in s.objects if o.type=='EMPTY' and o.name.startswith(('Adult','Child'))]:
|
||||
if ob.animation_data:
|
||||
for fc in ob.animation_data.action.fcurves:
|
||||
for k in fc.keyframe_points:
|
||||
if int(k.co.x) in [240,239]:k.interpolation='CONSTANT'
|
||||
# Sound: filtered noise breath, scratching and synthetic growl
|
||||
rate=24000;random.seed(72);samples=[];lp=0
|
||||
for i in range(rate*20):
|
||||
t=i/rate;lp=.90*lp+.10*random.uniform(-1,1)
|
||||
env=(.5+.5*math.sin(t*2.1))**3
|
||||
a=.07*lp*env if t<10 or t>17 else .006*lp
|
||||
for begin,end in [(7.1,8.3),(13,13.45)]:
|
||||
if begin<t<end:a+=.13*lp*(.5+.5*math.sin(t*90))
|
||||
if 13.15<t<16.1:
|
||||
e=math.exp(-(t-13.15)*1.25);a+=e*(.27*math.sin(2*math.pi*62*t+3*math.sin(t*17))+.25*lp)
|
||||
samples.append(int(max(-.9,min(.9,a))*32767))
|
||||
audio=os.path.join(OUT,'effects.wav')
|
||||
with wave.open(audio,'wb') as w:w.setparams((1,2,rate,len(samples),'NONE','not compressed'));w.writeframes(struct.pack('<%dh'%len(samples),*samples))
|
||||
s.sequence_editor_create();s.sequence_editor.sequences.new_sound('Generated ambience and effects',audio,channel=1,frame_start=1)
|
||||
s.render.image_settings.file_format='PNG';s.render.filepath=os.path.join(OUT,'frames','frame_')
|
||||
bpy.ops.file.pack_all()
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_preview.blend'))
|
||||
mode=sys.argv[-1]
|
||||
if mode=='previews':
|
||||
for f in [80,185,320]:
|
||||
s.frame_set(f);s.render.filepath=os.path.join(OUT,'preview_%03d.png'%f);bpy.ops.render.render(write_still=True)
|
||||
elif mode=='video':
|
||||
s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4';s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='MEDIUM';s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192;s.render.filepath=os.path.join(OUT,'halloween_preview.mp4');bpy.ops.render.render(animation=True)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Render the approved dark scene as a 20-second video."""
|
||||
import bpy, os
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview','v4')
|
||||
bpy.ops.wm.open_mainfile(filepath=os.path.join(OUT,'halloween_examples.blend'))
|
||||
s=bpy.context.scene
|
||||
# Animation quality: retain lights, fog and reflections; use fewer temporal samples.
|
||||
s.eevee.taa_render_samples=8;s.eevee.volumetric_samples=16
|
||||
s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100
|
||||
s.render.fps=24;s.frame_start=1;s.frame_end=480
|
||||
s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4'
|
||||
s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='MEDIUM'
|
||||
s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192
|
||||
s.render.filepath=os.path.join(OUT,'halloween_dark.mp4')
|
||||
# Confirm that all four visitors have animated leg joints in the saved scene.
|
||||
legs=[o for o in s.objects if 'leg pivot' in o.name]
|
||||
assert len(legs)==8
|
||||
s.frame_set(140);a=[o.evaluated_get(bpy.context.evaluated_depsgraph_get()).rotation_euler.x for o in legs]
|
||||
s.frame_set(148);b=[o.evaluated_get(bpy.context.evaluated_depsgraph_get()).rotation_euler.x for o in legs]
|
||||
assert all(abs(x-y)>.01 for x,y in zip(a,b))
|
||||
print('VERIFIED: eight moving leg joints')
|
||||
s.frame_set(1);bpy.ops.file.pack_all()
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_dark.blend'))
|
||||
bpy.ops.render.render(animation=True)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Decode selected video frames and save the editable scene with MP4 settings."""
|
||||
import bpy, os
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview')
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
s=bpy.context.scene;s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100;s.render.fps=24
|
||||
s.sequence_editor_create();strip=s.sequence_editor.sequences.new_movie('Finished video',os.path.join(OUT,'halloween_preview.mp4'),channel=1,frame_start=1)
|
||||
print('DECODED MOVIE',strip.frame_duration,strip.elements[0].orig_width,strip.elements[0].orig_height)
|
||||
s.render.image_settings.file_format='PNG'
|
||||
for frame in [1,80,120,185,239,241,310,316,320,360,440,480]:
|
||||
s.frame_set(frame);s.render.filepath=os.path.join(OUT,'frames','review_%03d.png'%frame);bpy.ops.render.render(write_still=True)
|
||||
bpy.ops.wm.open_mainfile(filepath=os.path.join(OUT,'halloween_preview.blend'))
|
||||
s=bpy.context.scene;s.frame_set(1)
|
||||
s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4';s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='MEDIUM';s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192;s.render.filepath='//halloween_preview.mp4'
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_preview.blend'))
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Decode selected video frames and save the editable scene with MP4 settings."""
|
||||
import bpy, os
|
||||
ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUT=os.path.join(ROOT,'preview','v4')
|
||||
os.makedirs(os.path.join(OUT,'frames'),exist_ok=True)
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
s=bpy.context.scene;s.render.resolution_x=1280;s.render.resolution_y=720;s.render.resolution_percentage=100;s.render.fps=24
|
||||
s.sequence_editor_create();strip=s.sequence_editor.sequences.new_movie('Finished video',os.path.join(OUT,'halloween_dark.mp4'),channel=1,frame_start=1)
|
||||
print('DECODED MOVIE',strip.frame_duration,strip.elements[0].orig_width,strip.elements[0].orig_height)
|
||||
s.render.image_settings.file_format='PNG'
|
||||
for frame in [1,120,128,136,144,152,185,310,322,334,440,480]:
|
||||
s.frame_set(frame);s.render.filepath=os.path.join(OUT,'frames','review_%03d.png'%frame);bpy.ops.render.render(write_still=True)
|
||||
bpy.ops.wm.open_mainfile(filepath=os.path.join(OUT,'halloween_dark.blend'))
|
||||
s=bpy.context.scene;s.frame_set(1)
|
||||
s.render.image_settings.file_format='FFMPEG';s.render.ffmpeg.format='MPEG4';s.render.ffmpeg.codec='H264';s.render.ffmpeg.constant_rate_factor='MEDIUM';s.render.ffmpeg.audio_codec='AAC';s.render.ffmpeg.audio_bitrate=192;s.render.filepath='//halloween_dark.mp4'
|
||||
bpy.ops.wm.save_as_mainfile(filepath=os.path.join(OUT,'halloween_dark.blend'))
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
source_dir=$project_dir
|
||||
pi_user=
|
||||
pi_host=
|
||||
remote_dir=
|
||||
apply=false
|
||||
install_service=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Projektdateien per SSH/rsync auf einen Raspberry Pi kopieren.
|
||||
|
||||
Aufruf:
|
||||
bash tools/upload_to_pi.sh --user BENUTZER --host HOST \
|
||||
--remote-dir /home/BENUTZER/halloween [--source ORDNER] [--install-service] [--apply]
|
||||
|
||||
Ohne --apply wird nur der geplante Aufruf angezeigt; es gibt keine Verbindung
|
||||
zum Pi. --apply legt den Zielordner an und kopiert die Dateien. Alte Dateien
|
||||
auf dem Pi werden nicht gelöscht.
|
||||
--install-service richtet nach dem Upload einen systemd-Autostart ein. Dazu
|
||||
muss die Python-Umgebung auf dem Pi bereits vorbereitet sein; sudo wird dort
|
||||
benötigt. Ohne --apply wird auch der Dienst nicht installiert.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Fehler: %s\n' "$*" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
while (($#)); do
|
||||
case $1 in
|
||||
--user|--host|--remote-dir|--source)
|
||||
(($# >= 2)) || die "Wert für $1 fehlt"
|
||||
case $1 in
|
||||
--user) pi_user=$2 ;;
|
||||
--host) pi_host=$2 ;;
|
||||
--remote-dir) remote_dir=$2 ;;
|
||||
--source) source_dir=$2 ;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--apply) apply=true; shift ;;
|
||||
--install-service) install_service=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) die "Unbekanntes Argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $pi_user =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]] || die 'Gültigen --user angeben'
|
||||
[[ $pi_host =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || die 'Gültigen --host angeben (Name oder IPv4-Adresse)'
|
||||
[[ $remote_dir =~ ^/[a-zA-Z0-9_./-]+$ && $remote_dir != / ]] || die 'Sicheren absoluten --remote-dir angeben'
|
||||
[[ /$remote_dir/ != *'/../'* && /$remote_dir/ != *'/./'* ]] || die 'Zielpfad darf keine . oder .. Segmente enthalten'
|
||||
[[ -d $source_dir ]] || die "Quellordner fehlt: $source_dir"
|
||||
source_dir=$(cd -- "$source_dir" && pwd -P)
|
||||
if $install_service; then
|
||||
[[ -f "$source_dir/tools/install_pi_service.sh" ]] || die 'tools/install_pi_service.sh fehlt in der Quelle'
|
||||
fi
|
||||
|
||||
command -v ssh >/dev/null || die 'ssh fehlt lokal'
|
||||
command -v rsync >/dev/null || die 'rsync fehlt lokal'
|
||||
|
||||
target="$pi_user@$pi_host"
|
||||
excludes=(
|
||||
--exclude=.git/
|
||||
--exclude=.venv/
|
||||
--exclude=__pycache__/
|
||||
--exclude=data/
|
||||
--exclude=preview/
|
||||
--exclude=references/
|
||||
--exclude=.env
|
||||
--exclude='*.pem'
|
||||
--exclude='*.key'
|
||||
--exclude='*.p12'
|
||||
--exclude='*.pfx'
|
||||
)
|
||||
ssh_cmd=(ssh -o StrictHostKeyChecking=ask "$target" "mkdir -p -- '$remote_dir'")
|
||||
rsync_cmd=(
|
||||
rsync -a --delay-updates --human-readable --info=stats2
|
||||
"${excludes[@]}"
|
||||
-e 'ssh -o StrictHostKeyChecking=ask'
|
||||
-- "$source_dir/" "$target:$remote_dir/"
|
||||
)
|
||||
service_cmd=(ssh -tt -o StrictHostKeyChecking=ask "$target" "cd -- '$remote_dir' && bash tools/install_pi_service.sh --apply")
|
||||
|
||||
printf 'Quelle: %s\nZiel: %s:%s/\n' "$source_dir" "$target" "$remote_dir"
|
||||
printf 'Ausgelassen: .git, .venv, data, preview, references, __pycache__, .env, Schlüsseldateien\n'
|
||||
if ! $apply; then
|
||||
printf '\nVorschau, noch keine Verbindung und keine Übertragung:\n'
|
||||
printf ' %q' "${ssh_cmd[@]}"; printf '\n'
|
||||
printf ' %q' "${rsync_cmd[@]}"; printf '\n'
|
||||
if $install_service; then
|
||||
printf ' %q' "${service_cmd[@]}"; printf '\n'
|
||||
fi
|
||||
printf '\nMit --apply ausführen.\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${ssh_cmd[@]}"
|
||||
"${rsync_cmd[@]}"
|
||||
printf 'Übertragung abgeschlossen.\n'
|
||||
if $install_service; then
|
||||
"${service_cmd[@]}"
|
||||
fi
|
||||
Reference in new issue
Block a user