diff --git a/compute.py b/compute.py index 2cdcd2f..49b6f69 100644 --- a/compute.py +++ b/compute.py @@ -65,6 +65,7 @@ box_color_r = None box_color_g = None box_color_b = None use_marker = 0 +camera_keyframes_raw = "" # 力开关 GRAVITY_FIELD = 1 # 均匀重力场 @@ -86,6 +87,27 @@ Z_MIN = None Z_MAX = None +def _load_camera_keyframes(path): + """读取 move_camera.txt,返回 JSON 数组字符串 [[frame,dist,el,az],...] 或空串。""" + if not os.path.exists(path): + print(f"[compute] 警告: 未找到 {path},跳过运动相机") + return "" + keyframes = [] + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + if len(parts) >= 4: + keyframes.append([int(parts[0]), float(parts[1]), + float(parts[2]), float(parts[3])]) + if not keyframes: + return "" + import json + return json.dumps(keyframes) + + def _to_text_value(value): """Convert numpy-heavy objects into JSON-friendly plain Python values.""" if isinstance(value, np.ndarray): @@ -642,7 +664,7 @@ def run_from_config(config, out_dir=None): global X_MIN, X_MAX, Y_MIN, Y_MAX, Z_MIN, Z_MAX global ball_radius, ball_color_r, ball_color_g, ball_color_b global box_color_r, box_color_g, box_color_b - global use_marker + global use_marker, camera_keyframes_raw global warmup_steps, sample_start, sample_end global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH global DRIVING_FORCE, DRIVER_DATA @@ -726,6 +748,17 @@ def run_from_config(config, out_dir=None): driver_path = os.path.join(out_dir, driver_rel) DRIVER_DATA = load_driver_file(driver_path, ATOM_IDS) + # 加载运动相机关键帧 + camera_keyframes_raw = "" + move_camera = int(config.get("move_camera", 0)) + camera_keyframes_raw = "" + if move_camera: + cam_rel = str(config.get("move_camera_file", os.path.join("input", "move_camera.txt"))) + cam_path = cam_rel + if out_dir is not None and not os.path.isabs(cam_rel): + cam_path = os.path.join(out_dir, cam_rel) + camera_keyframes_raw = _load_camera_keyframes(cam_path) + print(f"[compute] 使用算法: {METHOD}") print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键") if config.get("_skip_run", False): @@ -1464,7 +1497,8 @@ def run_simulation(save_trajectory=0): "atom_radii": ",".join(str(r) for r in ATOM_RADII), "camera_distance": str(config.get("camera_distance", 40.0)), "camera_elevation": str(config.get("camera_elevation", 0)), - "camera_azimuth": str(config.get("camera_azimuth", 0))} + "camera_azimuth": str(config.get("camera_azimuth", 0)), + "camera_keyframes": str(camera_keyframes_raw)} ) print(f"[compute] display.txt 已保存至: {disp_path} ({n_frames_actual} 帧)") diff --git a/draw.py b/draw.py index 19283b1..c8f3900 100644 --- a/draw.py +++ b/draw.py @@ -10,6 +10,7 @@ """ import numpy as np +import json import os import sys from vispy import app, scene @@ -554,9 +555,45 @@ print(f"[draw] 渲染方式: {mode_str}") print(f"[draw] 绘图参数: ball_radius={ball_radius}, box_color=({box_color_r:.2f},{box_color_g:.2f},{box_color_b:.2f}), alpha={alpha_list}") +# 运动相机关键帧(可选) +_CAM_KF = json.loads(h.get("camera_keyframes", "null")) if h.get("camera_keyframes") else None + + # =========================================================================== # 每帧回调:仅推进帧索引,从预存数组读取位置,零物理计算 # =========================================================================== +def _interp_camera(f_idx): + """根据关键帧插值相机位置。""" + if not _CAM_KF or len(_CAM_KF) < 2: + return + # 找到当前帧对应的关键帧区间 + n_kf = len(_CAM_KF) + # 映射到关键帧时间线 + total_kf_frames = _CAM_KF[-1][0] + if total_kf_frames <= 0: + return + # 循环播放关键帧 + t = (f_idx / N_FRAMES) * total_kf_frames + # 二分查找区间 + lo, hi = 0, n_kf - 1 + while hi - lo > 1: + mid = (lo + hi) // 2 + if _CAM_KF[mid][0] <= t: + lo = mid + else: + hi = mid + f0, f1 = _CAM_KF[lo], _CAM_KF[hi] + if f1[0] - f0[0] == 0: + return + frac = (t - f0[0]) / (f1[0] - f0[0]) + dist = f0[1] + (f1[1] - f0[1]) * frac + elev = f0[2] + (f1[2] - f0[2]) * frac + azim = f0[3] + (f1[3] - f0[3]) * frac + view.camera.distance = dist + view.camera.elevation = elev + view.camera.azimuth = azim + + def update(event): global frame_idx frame_idx = (frame_idx + 1) % N_FRAMES # 循环播放 @@ -568,6 +605,9 @@ def update(event): if bond_lines is not None and len(BOND_PAIRS) > 0: _update_bond_positions(frame_idx) + # 运动相机:按关键帧插值 + _interp_camera(frame_idx) + # 信息面板显示 plot_atom 的数据 x = float(DISP_X[frame_idx]) y = float(DISP_Y[frame_idx]) diff --git a/dynamics.py b/dynamics.py index 7274b36..f391bec 100644 --- a/dynamics.py +++ b/dynamics.py @@ -32,6 +32,31 @@ def _fmt_alpha(v): return str(float(v)) +def _load_camera_kf(config, runtime_base): + """加载 move_camera.txt → JSON 字符串,供 display.txt header 使用。""" + if not int(config.get("move_camera", 0)): + return "" + cam_file = str(config.get("move_camera_file", + os.path.join("input", "move_camera.txt"))) + cam_path = cam_file + if not os.path.isabs(cam_file): + cam_path = os.path.join(runtime_base, cam_file) + if not os.path.exists(cam_path): + return "" + import json + kfs = [] + with open(cam_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + if len(parts) >= 4: + kfs.append([int(parts[0]), float(parts[1]), + float(parts[2]), float(parts[3])]) + return json.dumps(kfs) if kfs else "" + + def read_optional_index(data, key, default_value): """Read an optional integer index from structured txt metadata.""" if key not in data: @@ -257,7 +282,8 @@ def run_case(config_path, runtime_base, input_dir="input", output_dir="output", "atom_radii": _fmt_alpha(data.get("atom_radii", [])), "camera_distance": str(config.get("camera_distance", 40.0)), "camera_elevation": str(config.get("camera_elevation", 0)), - "camera_azimuth": str(config.get("camera_azimuth", 0))} + "camera_azimuth": str(config.get("camera_azimuth", 0)), + "camera_keyframes": _load_camera_kf(config, str(runtime_base))} n_frames = len(indices) compute.save_display_txt( diff --git a/examples/case06/input/input.txt b/examples/case06/input/input.txt index d0be85b..addbf2f 100644 --- a/examples/case06/input/input.txt +++ b/examples/case06/input/input.txt @@ -105,3 +105,4 @@ box_color_b: 0.85 camera_distance: 40.0 # 摄像机到场景中心的距离 camera_elevation: 0 # 俯仰角(度),负值=俯视 camera_azimuth: 0 # 方位角(度) +move_camera: 0 # 0=固定视角, 1=按 move_camera.txt 运动 diff --git a/examples/case06/input/move_camera.txt b/examples/case06/input/move_camera.txt new file mode 100644 index 0000000..fc2a020 --- /dev/null +++ b/examples/case06/input/move_camera.txt @@ -0,0 +1,11 @@ +# move_camera.txt — 摄像机关键帧动画 +# 格式: frame distance elevation azimuth +# frame: 关键帧序号(对应动画帧,非计算步) +# distance: 到场景中心距离 +# elevation: 俯仰角(度,负值=俯视) +# azimuth: 方位角(度,沿 Y 轴顺时针旋转) +# +# 两帧之间线性插值,到达最后一帧后循环 +0 40.0 0 0 +100 80.0 -30 180 +200 40.0 0 360