Files
ShortVideoGenerator/projects/water-hydrogen-bond-surface-tension/blender/verify_model.py
T

113 lines
7.1 KiB
Python
Raw 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
from bpy_extras.object_utils import world_to_camera_view
def verify(source):
assert source.is_file(), "원본 파일 없음"
bpy.ops.wm.open_mainfile(filepath=str(source))
expected = {"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
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 {"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"])