Files

254 lines
13 KiB
Python

"""승인된 물 소재 자산을 복사해 정식 기준 원본과 애니메이션 원본을 만든다."""
import argparse
import hashlib
import json
import math
from pathlib import Path
import sys
import bpy
from mathutils import Vector
ROOT=Path(__file__).resolve().parents[3]
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
APPROVED=PROJECT/'stages/08-scenes/r3/generated-source/preview.blend'
APPROVED_HASH='ba0850a2148824cc8416704c03f14e5714ca01a16620a4eb26981e238af78c73'
parser=argparse.ArgumentParser()
parser.add_argument('--stage',choices=['references','images'],required=True)
args=parser.parse_args(sys.argv[sys.argv.index('--')+1:])
def clone(obj,name,scene,parent=None):
result=obj.copy()
if obj.data:
result.data=obj.data.copy()
result.name=name
result.animation_data_clear()
result.parent=parent
scene.collection.objects.link(result)
return result
def studio(name,base,bounds,scale):
scene=bpy.data.scenes.new(name)
scene.world=base.world
scene.render.engine='BLENDER_EEVEE'
scene.render.fps=50
scene.render.resolution_x,scene.render.resolution_y=1080,1920
scene.eevee.taa_render_samples=64
scene.view_settings.view_transform='Standard'
for obj in base.objects:
if obj.type in ('CAMERA','LIGHT') or obj.get('role')=='background':
item=clone(obj,name+obj.type+obj.name,scene)
if item.type=='CAMERA':
scene.camera=item
item.data.ortho_scale=scale
scene.frame_start,scene.frame_end=bounds
return scene
def molecule(scene,name,location,size=1,rotation=0):
template=bpy.data.objects['PairA']
root=clone(template,name,scene)
root.location=location
root.rotation_euler=(0,0,rotation)
root.scale=(size,)*3
for child in template.children:
clone(child,name+child.name[len('PairA'):],scene,root)
return root
def droplet(scene,name,location,scale):
obj=clone(bpy.data.objects['Droplet'],name,scene)
obj.location=location
obj.scale=scale
return obj
def anchor(scene,name,location,label):
obj=bpy.data.objects.new(name,None)
scene.collection.objects.link(obj)
obj.location=location
obj['anchor']=label
return obj
def mark(obj,label):
obj['anchor']=label
def collection(name,objects):
group=bpy.data.collections.new(name)
group.use_fake_user=True
for obj in objects:
group.objects.link(obj)
return group
def animate_network(scene,prefix,start,turn,end):
# 온전한 분자를 움직이고 H···O 원통을 실제 원자 위치로 갱신한다.
roots=[scene.objects[prefix+str(i)] for i in range(10)]
for i,root in enumerate(roots):
position=root.location.copy()
for frame,delta in [(start,0),(turn,.09*math.sin(i+1)),(end,.06*math.cos(i))]:
root.location=position+Vector((delta,0,0))
root.keyframe_insert('location',frame=frame)
template=bpy.data.objects['LiquidDash0_1_0']
pairs=[(0,1),(1,2),(3,4),(3,7),(4,5),(4,6),(4,7),(5,6),(5,8),(8,9)]
for i,j in pairs:
hydrogen,oxygen=scene.objects[prefix+str(i)+'_H1'],scene.objects[prefix+str(j)+'_O']
for segment in range(6):
obj=clone(template,prefix+'Dash'+str(i)+'_'+str(j)+'_'+str(segment),scene)
obj['hydrogen'],obj['oxygen']=hydrogen.name,oxygen.name
for frame in [start,1779,turn,2257,end]:
scene.frame_set(frame)
bpy.context.view_layer.update()
a,b=hydrogen.matrix_world.translation,oxygen.matrix_world.translation
direction=b-a
obj.location=a+direction*(.18+segment*.11+.031)
obj.rotation_euler=direction.to_track_quat('Z','Y').to_euler()
active=not ((i,j)==(3,4) and frame>=turn) and not ((i,j)==(3,7) and frame<turn) and not ((i,j)==(4,5) and frame>=2257) and not ((i,j)==(4,6) and frame<2257)
obj.scale=(1 if active else .001,1 if active else .001,direction.length*.062/.1)
for path in ['location','rotation_euler','scale']:
obj.keyframe_insert(path,frame=frame)
if args.stage=='references':
assert hashlib.sha256(APPROVED.read_bytes()).hexdigest()==APPROVED_HASH, '승인 원본 해시 불일치'
bpy.ops.wm.open_mainfile(filepath=str(APPROVED))
pair=bpy.data.scenes['BondPair']
drop=bpy.data.scenes['DropletStretch']
interface=bpy.data.scenes['Interface']
opening=studio('OpeningDroplet',drop,(1,397),10.5)
obj=droplet(opening,'OpeningWater',(0,0,0),(1,1,1))
mark(obj,'opening_drop')
anchor(opening,'OpeningMoleculeInset',(1.65,0,1.7),'molecule_inset')
single=studio('SingleMolecule',pair,(397,905),9)
molecule(single,'SingleWater',(0,0,-.4),1.8)
for suffix,label in [('O','single_O_delta_minus'),('H1','single_H_delta_plus'),('H2','single_H2_delta_plus')]:
mark(single.objects['SingleWater_'+suffix],label)
network=studio('LiquidNetwork',interface,(1637,2469),13)
for i in range(10):
old=interface.objects['Liquid'+str(i)]
molecule(network,'Network'+str(i),(old.location.x,old.location.y,old.location.z+1.3),.58,old.rotation_euler.z)
mark(network.objects['Network4'],'network_interior')
comparison=studio('EqualVolumeComparison',drop,(4077,4370),12.5)
size=.72
droplet(comparison,'ComparisonLong',(-1.65,0,0),(size*1.65,size/math.sqrt(1.65),size/math.sqrt(1.65)))
droplet(comparison,'ComparisonRound',(1.65,0,0),(size,)*3)
mark(comparison.objects['ComparisonLong'],'equal_volume_left')
mark(comparison.objects['ComparisonRound'],'equal_volume_right')
conditions=studio('ShapeConditions',drop,(4370,4889),12.5)
droplet(conditions,'SmallFreeDrop',(-1.65,0,.2),(.58,)*3)
# 큰 접촉 방울은 같은 부피 비교가 아닌 별도 조건 모형이다.
contact=droplet(conditions,'LargeContactDrop',(1.55,0,-.275),(1.02,1.02,.75/1.3))
mark(conditions.objects['SmallFreeDrop'],'small_free_condition')
mark(contact,'large_contact_condition')
bpy.context.window.scene=conditions
bpy.ops.mesh.primitive_cube_add(size=1,location=(1.55,0,-1.15))
floor=bpy.context.object
floor.name='ContactFloor'
floor.scale=(3.1,2.5,.25)
floor_material=bpy.data.materials.new('ContactFloorCream')
floor_material.diffuse_color=(.78,.73,.62,1)
floor_material.use_nodes=True
floor_material.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value=(.78,.73,.62,1)
floor_material.node_tree.nodes['Principled BSDF'].inputs['Roughness'].default_value=.8
floor.data.materials.append(floor_material)
floor['role']='condition_floor'
mark(floor,'contact_floor')
answer=studio('AnswerDroplet',drop,(4889,5529),12.5)
droplet(answer,'AnswerWater',(0,0,-.7),(1,)*3)
mark(answer.objects['AnswerWater'],'answer_macro_drop')
for i,position in enumerate([(-1.1,0,1.8),(0,.1,2.05),(1.1,0,1.8)]):
molecule(answer,'AnswerMolecule'+str(i),position,.55,i*.2)
mark(answer.objects['AnswerMolecule0_H1'],'answer_inset_H')
mark(answer.objects['AnswerMolecule1_O'],'answer_inset_O')
mark(answer.objects['AnswerMolecule2'],'answer_inset_extent')
mark(pair.objects['PairA_H1'],'pair_H_delta_plus')
mark(pair.objects['PairB_O'],'pair_O_delta_minus')
mark(interface.objects['Liquid4'],'water_interior')
mark(interface.objects['Liquid1'],'water_surface')
anchor(interface,'InterfaceAirAnchor',(0,0,1.45),'air_region')
anchor(interface,'InterfaceWaterAnchor',(0,0,-2.95),'cutaway_water_region')
mark(drop.objects['Droplet'],'same_volume_drop')
collection('StudioAssets',[o for s in bpy.data.scenes for o in s.objects if o.get('role')=='background'])
collection('StyleAssets',[o for s in bpy.data.scenes for o in s.objects if o.get('role')=='background'])
collection('DropletStudioAssets',[o for s in bpy.data.scenes if s.name in ['OpeningDroplet','DropletStretch','AnswerDroplet'] for o in s.objects if o.get('role')=='background'])
collection('MoleculeStudioAssets',[o for s in bpy.data.scenes if s.name in ['SingleMolecule','BondPair','LiquidNetwork'] for o in s.objects if o.get('role')=='background'])
collection('ComparisonStudioAssets',[o for s in bpy.data.scenes if s.name in ['EqualVolumeComparison','ShapeConditions'] for o in s.objects if o.get('role')=='background'])
collection('MoleculeAssets',[o for o in bpy.data.objects if o.get('role') in ('molecule','atom','covalent','hydrogen_bond')])
collection('DropletAssets',[o for o in bpy.data.objects if o.get('role')=='same_volume_water'])
collection('InterfaceAssets',[interface.objects['WaterCutawayBody']])
# 실제 기준 원본에도 액체 연결 자산을 구성하여 후속 원본이 새 경로만 참조하지 않게 한다.
bpy.context.window.scene=network
animate_network(network,'Network',1637,2019,2469)
bpy.context.window.scene=answer
# 답 회수에는 거시 방울과 공간적으로 분리된 분자 개념도가 공존한다.
for i,j in [(0,1),(1,2)]:
template=bpy.data.objects['LiquidDash0_1_0']
h,o=answer.objects['AnswerMolecule'+str(i)+'_H1'],answer.objects['AnswerMolecule'+str(j)+'_O']
bpy.context.view_layer.update()
a,b=h.matrix_world.translation,o.matrix_world.translation
for n in range(6):
obj=clone(template,'AnswerDash'+str(i)+'_'+str(j)+'_'+str(n),answer)
obj['hydrogen'],obj['oxygen']=h.name,o.name
direction=b-a
obj.location=a+direction*(.18+n*.11+.031)
obj.rotation_euler=direction.to_track_quat('Z','Y').to_euler()
obj.scale=(1,1,direction.length*.062/.1)
for scene in bpy.data.scenes:
scene['approved_source_sha256']=APPROVED_HASH
destination=PROJECT/'stages/10-references/r2/generated-source/references.blend'
else:
artifact=json.loads((PROJECT/'stages/10-references/r2/artifact.json').read_text(encoding='utf-8'))
result=artifact['payload']['result']
source=ROOT/result['scene_file']['path']
assert result['scene_file']['verified'] and hashlib.sha256(source.read_bytes()).hexdigest()==result['scene_file']['sha256']
assert len(result['items'])==7 and all((ROOT/item['media']['path']).is_file() and item['media']['verified'] for item in result['items'])
bpy.ops.wm.open_mainfile(filepath=str(source))
for scene in bpy.data.scenes:
scene['reference_source_sha256']=result['scene_file']['sha256']
drop=bpy.data.objects['Droplet']
expression='1+0.85*min(max((frame-3173)/200,0),1)*(1-min(max((frame-3657)/196,0),1))'
drop.animation_data.drivers[0].driver.expression=expression
for axis in [1,2]:
drop.animation_data.drivers[axis].driver.expression='('+expression+')**(-0.5)'
# 액체 점선의 위치·방향을 프레임마다 실제 H/O 평가 위치에 맞춰 굽는다.
network=bpy.data.scenes['LiquidNetwork']
bpy.context.window.scene=network
segments=[obj for obj in network.objects if obj.get('role')=='hydrogen_bond']
for obj in segments:
obj.animation_data_clear()
for frame in range(1637,2470):
network.frame_set(frame)
bpy.context.view_layer.update()
for obj in segments:
h,o=network.objects[obj['hydrogen']],network.objects[obj['oxygen']]
a,b=h.matrix_world.translation,o.matrix_world.translation
direction=b-a
segment=int(obj.name.rsplit('_',1)[1])
obj.location=a+direction*(.18+segment*.11+.031)
obj.rotation_euler=direction.to_track_quat('Z','Y').to_euler()
pair=(h.parent.name,o.parent.name)
first=min(max((frame-1779)/240,0),1)
second=min(max((frame-2019)/238,0),1)
weight={('Network3','Network4'):1-first,('Network3','Network7'):first,('Network4','Network5'):1-second,('Network4','Network6'):second}.get(pair,1)
obj.scale=(max(weight,.001),max(weight,.001),direction.length*.062/.1)
for path in ['location','rotation_euler','scale']:
obj.keyframe_insert(path,frame=frame)
# 답의 수소결합은 영구 끈이 아니며 연결이 약해졌다가 다시 나타난다.
answer=bpy.data.scenes['AnswerDroplet']
for obj in answer.objects:
if obj.get('role')=='hydrogen_bond':
original=obj.scale.z
for frame,active in [(4889,1),(5100,1),(5160,.001),(5229,1),(5529,1)]:
obj.scale=(active,active,original)
obj.keyframe_insert('scale',frame=frame)
destination=PROJECT/'stages/11-images/r2/generated-source/animation.blend'
for scene in bpy.data.scenes:
bpy.context.window.scene=scene
scene.frame_set(scene.frame_start)
destination.parent.mkdir(parents=True,exist_ok=True)
assert not destination.exists(), '제출 원본 덮어쓰기 금지'
bpy.ops.wm.save_as_mainfile(filepath=str(destination))
print('정식 원본 저장',destination)