물 영상의 검증된 3D 대표 시제품과 새 장면 설계 작성

This commit is contained in:
NINI
2026-10-10 00:42:16 +09:00
parent 28a1861e9e
commit a97a778f7a
20 changed files with 3690 additions and 15 deletions
@@ -0,0 +1,112 @@
"""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"])