Files

191 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Blender 내부에서 실제 원본·구조·평가 부피를 검사한다."""
import argparse
import hashlib
import json
from pathlib import Path
import sys
import bpy
import bmesh
import numpy as np
from mathutils import Vector
from bpy_extras.object_utils import world_to_camera_view
def containment(scene):
"""전체 프레임의 실제 mesh 정점을 물 영역·화면 윤곽과 대조한다."""
bpy.context.window.scene = scene
body = scene.objects['WaterCutawayBody']
deps = bpy.context.evaluated_depsgraph_get()
evaluated = body.evaluated_get(deps)
mesh = evaluated.to_mesh()
water = np.array([tuple(evaluated.matrix_world @ v.co) for v in mesh.vertices])
faces=[list(face.vertices) for face in mesh.polygons]
evaluated.to_mesh_clear()
low, high = water.min(axis=0), water.max(axis=0)
# 실제 물 표시 mesh에서 경계를 얻고 중심·별도 메타데이터만으로 판정하지 않는다.
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])
screen = (water @ projection + offset) * (1080,1920)
face_polygons=[screen[face] for face in faces]
points = sorted(set(map(tuple,screen)))
def cross(a,b,c):
return (b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0])
lower, upper = [], []
for chain, vertices in ((lower,points),(upper,reversed(points))):
for p in vertices:
while len(chain)>=2 and cross(chain[-2],chain[-1],p)<=0:
chain.pop()
chain.append(p)
hull = np.array(lower[:-1]+upper[:-1])
minimum = np.full(3,np.inf)
pixel_margin = float('inf')
vertex_count = 0
for logical in range(2468,3173):
scene.frame_set(logical+1)
deps = bpy.context.evaluated_depsgraph_get()
for obj in scene.objects:
role = obj.get('role')
if role not in ('atom','covalent','hydrogen_bond') or (role=='hydrogen_bond' and obj.scale.x<=.0011):
continue
evaluated = obj.evaluated_get(deps)
mesh = evaluated.to_mesh()
local = np.array([tuple(v.co) for v in mesh.vertices])
transform = np.array(evaluated.matrix_world)
vertices = local @ transform[:3,:3].T + transform[:3,3]
evaluated.to_mesh_clear()
margin = np.minimum(vertices-low,high-vertices).min(axis=0)
assert margin.min()>=.03, f'물 영역 밖 또는 여백 부족: F{logical} {obj.name}, 축 여백 {margin.tolist()}'
minimum = np.minimum(minimum,margin)
projected = (vertices @ projection + offset) * (1080,1920)
assert (projected.min(axis=0)>0).all() and (projected.max(axis=0)<(1080,1920)).all(), '분자 화면 잘림'
covered=np.zeros(len(projected),dtype=bool)
for polygon in face_polygons:
following=np.roll(polygon,-1,axis=0)
area=sum(a[0]*b[1]-b[0]*a[1] for a,b in zip(polygon,following))
if abs(area)<.001:
continue
inside=np.ones(len(projected),dtype=bool)
for a,b in zip(polygon,following):
edge=b-a
signed=(edge[0]*(projected[:,1]-a[1])-edge[1]*(projected[:,0]-a[0]))*(1 if area>0 else -1)
inside &= signed>=-.001
covered |= inside
assert covered.all(), f'실제 파란 절개 면 투영 밖: F{logical} {obj.name}'
for a,b in zip(hull,np.roll(hull,-1,axis=0)):
edge = b-a
distance = (edge[0]*(projected[:,1]-a[1])-edge[1]*(projected[:,0]-a[0]))/np.linalg.norm(edge)
assert distance.min()>=0, f'파란 물 영역 투영 밖: F{logical} {obj.name}'
pixel_margin = min(pixel_margin,float(distance.min()))
vertex_count += len(vertices)
# 앞면과 윗면은 실제 열린 절개이며 물의 고체 껍질을 만들지 않는다.
assert len(body.data.polygons)==4, '물 절개 면 수 불일치'
for face in body.data.polygons:
points=np.array([tuple(body.matrix_world @ body.data.vertices[i].co) for i in face.vertices])
assert not np.allclose(points[:,1],low[1]) and not np.allclose(points[:,2],high[2]), '앞면·윗면 절개가 닫힘'
return dict(logical_frame_start=2468,logical_frame_end=3172,frames=705,water_bounds=[low.tolist(),high.tolist()],minimum_axis_margin=minimum.tolist(),minimum_projected_margin_px=pixel_margin,evaluated_vertices=vertex_count,mesh_roles=['atom','covalent','active_hydrogen_bond'],open_faces=['front','top'],projection_check='actual_face_union_and_outer_hull')
def verify(source, expected=None):
assert source.is_file(), "원본 파일 없음"
bpy.ops.wm.open_mainfile(filepath=str(source))
expected = expected or {"BondPair": (905, 1637), "Interface": (2469, 3173), "DropletStretch": (3173, 4077)}
assert set(bpy.data.scenes) and set(s.name for s in bpy.data.scenes) == set(expected), "대표 Scene 누락/혼입"
assert not any(d.library for d in bpy.data.objects), "외부 객체"
assert not bpy.data.images and not bpy.data.fonts and not bpy.data.movieclips, "외부 자산"
checks = {}
for name, bounds in expected.items():
scene = bpy.data.scenes[name]
bpy.context.window.scene = scene
assert (scene.frame_start, scene.frame_end) == bounds, "실측 범위를 덮지 못함"
assert scene.camera and scene.render.fps == 50
assert (scene.render.resolution_x, scene.render.resolution_y) == (1080, 1920)
assert scene.render.engine == "BLENDER_EEVEE"
molecules = [o for o in scene.objects if o.get("role") == "molecule"]
for mol in molecules:
atoms = [o for o in mol.children if o.get("role") == "atom"]
assert sorted(o["element"] for o in atoms) == ["H", "H", "O"], "O1H2 위반"
assert all(o.type == "MESH" and len(o.data.vertices) > 20 for o in atoms)
assert all(o.data.materials[0].name==('O_Coral' if o['element']=='O' else 'H_Ivory') for o in atoms), '원자 색 재질 불일치'
oxygen = next(o for o in atoms if o["element"] == "O")
hydrogen = [o for o in atoms if o["element"] == "H"]
assert (hydrogen[0].location - oxygen.location).cross(hydrogen[1].location - oxygen.location).length > .01, "V형태 아님"
bonds = [o for o in mol.children if o.get("role") == "covalent"]
assert len(bonds) == 2 and all(o.type == "MESH" for o in bonds), "내부 실선 두 개 누락"
assert all(o.data.materials[0].name=='OH_Navy' for o in bonds), '내부 실선 재질 불일치'
assert all(min((bond.location - (h.location+oxygen.location)/2).length for h in hydrogen)<.001 for bond in bonds), "실선의 O–H 위치 불일치"
for segment in [o for o in scene.objects if o.get("role") == "hydrogen_bond"]:
h, oxygen = (scene.objects[segment[k]] for k in ("hydrogen", "oxygen"))
assert h["element"] == "H" and oxygen["element"] == "O" and h.parent != oxygen.parent, "H···O 분자간 점선 위반"
assert segment.type == "MESH" and segment.dimensions.length > 0
assert segment.data.materials[0].name=='HO_Gold', '분자간 점선 재질 불일치'
checks[name] = {"molecules": len(molecules), "frame_range": list(bounds)}
assert checks["BondPair"]["molecules"] == 2 and checks["Interface"]["molecules"] >= 8
pair = bpy.data.scenes["BondPair"]
bpy.context.window.scene = pair
pair.frame_set(1018)
before = pair.objects["PairB"].matrix_world.translation.copy()
pair.frame_set(1117)
assert (pair.objects["PairB"].matrix_world.translation - before).length > .1, "실제 3D 이동 없음"
interface = bpy.data.scenes['Interface']
bpy.context.window.scene = interface
water_containment = containment(interface)
connection_sets, neighborhoods = [], []
for frame in (2807,2907,3051):
interface.frame_set(frame)
active = sorted({(o['hydrogen'],o['oxygen']) for o in interface.objects if o.get('role')=='hydrogen_bond' and o.scale.x > .5})
connection_sets.append(active)
for segment in [o for o in interface.objects if o.get('role')=='hydrogen_bond']:
h, oxygen = (interface.objects[segment[k]] for k in ('hydrogen','oxygen'))
a,b = h.matrix_world.translation, oxygen.matrix_world.translation
assert (segment.location-a).cross(b-a).length < .002, '점선이 실제 H···O 선분에서 벗어남'
counts=[]
for name in ('Liquid4','Liquid1'):
center=interface.objects[name].matrix_world.translation
neighbors=[o.matrix_world.translation-center for o in interface.objects if o.get('role')=='molecule' and o.name!=name and (o.matrix_world.translation-center).length<2.2]
counts.append(dict(neighbors=len(neighbors),above=sum(p.z>.7 for p in neighbors)))
assert counts[0]['neighbors']>counts[1]['neighbors'] and counts[0]['above']>0 and counts[1]['above']==0, '내부/표면 이웃 배치 차이 없음'
neighborhoods.append(counts)
assert connection_sets[0] != connection_sets[-1], '실제 활성 연결 쌍이 바뀌지 않음'
scene = bpy.data.scenes["DropletStretch"]
bpy.context.window.scene = scene
droplet = scene.objects["Droplet"]
volumes, scales = [], []
for frame in (3173, 3223, 3273, 3323, 3373):
scene.frame_set(frame)
deps = bpy.context.evaluated_depsgraph_get()
evaluated = droplet.evaluated_get(deps)
mesh = evaluated.to_mesh()
bm = bmesh.new()
bm.from_mesh(mesh)
assert all(e.is_manifold for e in bm.edges), "닫힌 mesh 아님"
bm.transform(evaluated.matrix_world)
volumes.append(abs(bm.calc_volume(signed=True)))
scales.append(list(evaluated.scale))
bm.free()
evaluated.to_mesh_clear()
relative = max(abs(v / volumes[0] - 1) for v in volumes)
assert relative <= .01, "중간 프레임 포함 부피 변화 1% 초과"
assert abs(scales[-1][0] - scales[0][0]) > .5, "변형이 실제 평가되지 않음"
anchors = {}
for name, logical in (("BondPair", 1219), ("Interface", 3050), ("DropletStretch", 3372)):
scene = bpy.data.scenes[name]
bpy.context.window.scene = scene
scene.frame_set(logical + 1)
anchors[name] = {"logical_frame":logical,"labels":{}}
for obj in scene.objects:
if obj.get("label"):
p = world_to_camera_view(scene, scene.camera, obj.matrix_world.translation)
anchors[name]['labels'][obj["label"]] = {"x": round(p.x * 1080, 2), "y": round((1-p.y)*1920, 2), "depth": round(p.z, 2)}
return {"water_containment":water_containment,"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(), "blender_version": bpy.app.version_string, "scenes": checks, "interface_frames_blender":[2807,2907,3051], "interface_active_pairs":connection_sets,"interface_neighborhoods":neighborhoods,"volume_frames_blender": [3173,3223,3273,3323,3373], "volumes": volumes, "relative_volume_change": relative, "evaluated_scales": scales, "label_anchors": anchors, "text_baked": False, "physical_simulation": False}
if __name__ == "__main__":
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:])
result = verify(Path(args.source).resolve())
Path(args.out).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print("MODEL_GREEN", result["source_sha256"], result["relative_volume_change"])