86 lines
4.5 KiB
Python
86 lines
4.5 KiB
Python
"""1080×1920 실제 카메라 투영으로 T5 주석 위치와 필요한 매 프레임 키를 내보낸다."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import bpy
|
|
import numpy as np
|
|
from mathutils import Vector
|
|
from bpy_extras.object_utils import world_to_camera_view
|
|
ROOT=Path(__file__).resolve().parents[3]
|
|
PROJECT=ROOT/'projects/water-hydrogen-bond-surface-tension'
|
|
parser=argparse.ArgumentParser()
|
|
parser.add_argument('--source',required=True)
|
|
parser.add_argument('--out',required=True)
|
|
args=parser.parse_args(sys.argv[sys.argv.index('--')+1:])
|
|
source=Path(args.source).resolve()
|
|
bpy.ops.wm.open_mainfile(filepath=str(source))
|
|
cuts=json.loads((PROJECT/'stages/07-cuts/r1/artifact.json').read_text(encoding='utf-8'))['payload']['cuts']
|
|
plans=json.loads((PROJECT/'stages/08-scenes/r3/artifact.json').read_text(encoding='utf-8'))['payload']['scenes']
|
|
cache={}
|
|
|
|
|
|
def bbox_objects(obj):
|
|
return [obj] if obj.type=='MESH' else [child for child in obj.children if child.type=='MESH']
|
|
|
|
|
|
def coordinates(obj,deps):
|
|
if obj.name not in cache:
|
|
assert not obj.modifiers and not obj.data.shape_keys, '정점 캐시가 불가능한 변형 mesh'
|
|
evaluated=obj.evaluated_get(deps)
|
|
mesh=evaluated.to_mesh()
|
|
cache[obj.name]=np.array([tuple(v.co) for v in mesh.vertices])
|
|
evaluated.to_mesh_clear()
|
|
matrix=np.array(obj.evaluated_get(deps).matrix_world)
|
|
return cache[obj.name] @ matrix[:3,:3].T + matrix[:3,3]
|
|
|
|
|
|
result=[]
|
|
for cut,plan in zip(cuts,plans):
|
|
scene=bpy.data.scenes[plan['start_state'].split(';')[0].split('=')[1]]
|
|
bpy.context.window.scene=scene
|
|
assert scene.camera.data.type=='ORTHO'
|
|
origin=world_to_camera_view(scene,scene.camera,Vector((0,0,0)))
|
|
projection=np.array([tuple(world_to_camera_view(scene,scene.camera,Vector(b))-origin)[:2] for b in [(1,0,0),(0,1,0),(0,0,1)]])
|
|
offset=np.array(tuple(origin)[:2])
|
|
objects=[obj for obj in scene.objects if obj.get('anchor')]
|
|
start,end=cut['start_ms']//20,cut['end_ms']//20
|
|
tracks={obj['anchor']:[] for obj in objects}
|
|
states=[]
|
|
for frame in range(start,end+1):
|
|
scene.frame_set(frame+1)
|
|
deps=bpy.context.evaluated_depsgraph_get()
|
|
labels={}
|
|
for obj in objects:
|
|
center=np.array(tuple(obj.matrix_world.translation)) @ projection + offset
|
|
target=[round(float(center[0]*1080),2),round(float((1-center[1])*1920),2)]
|
|
parts=bbox_objects(obj)
|
|
box=None
|
|
if parts:
|
|
vertices=np.vstack([coordinates(part,deps) for part in parts])
|
|
projected=vertices @ projection + offset
|
|
px=projected[:,0]*1080
|
|
py=(1-projected[:,1])*1920
|
|
box=[round(float(px.min()),2),round(float(py.min()),2),round(float(px.max()),2),round(float(py.max()),2)]
|
|
label=[round((box[0]+box[2])/2,2),round(box[1]-32,2)]
|
|
else:
|
|
label=target
|
|
assert 0<label[0]<1080 and 0<label[1]<1920, '주석 제안점 화면 밖'
|
|
labels[obj['anchor']]=dict(object_name=obj.name,target_pixel=target,label_pixel=label,bbox_px=box)
|
|
tracks[obj['anchor']].append([frame,*label,*target])
|
|
if frame in [start,(start+end)//2,end]:
|
|
states.append(dict(logical_frame=frame,blender_frame=frame+1,anchors=labels))
|
|
motion=[]
|
|
for label,values in tracks.items():
|
|
changing=any(v[1:]!=values[0][1:] for v in values[1:])
|
|
keys=[]
|
|
for index,value in enumerate(values):
|
|
if index in [0,len(values)-1] or value[1:]!=values[index-1][1:]:
|
|
keys.append(value)
|
|
motion.append(dict(anchor_id=label,animated=changing,columns=['logical_frame','label_x','label_y','target_x','target_y'],keys=keys if changing else [values[0],values[-1]]))
|
|
result.append(dict(cut_id=cut['id'],scene_name=scene.name,camera_name=scene.camera.name,start_frame=start,end_frame=end,states=states,motion=motion))
|
|
value=dict(source_path=source.relative_to(ROOT).as_posix(),source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(),width=1080,height=1920,coordinate_system='top_left_pixel',logical_to_blender='logical_frame + 1',label_note='label_pixel은 실제 mesh 위 32px의 배치 시작점이다. 합성 단계에서 글자 크기·충돌·자막 여백을 실제 합성 화면으로 검수한다. bbox_px와 target_pixel은 실제 카메라 투영이다.',cuts=result)
|
|
Path(args.out).write_text(json.dumps(value,ensure_ascii=False,separators=(',',':'))+'\n',encoding='utf-8')
|
|
print('실제 카메라 주석 앵커 GREEN',len(result))
|