feat: 运动相机支持 + move_camera.txt 关键帧驱动

input.txt 新增:
  move_camera: 0  # 0=固定视角, 1=按 move_camera.txt 运动

move_camera.txt 格式(4列:帧号 距离 俯仰角 方位角):
  0    40.0   0     0
  100  80.0  -30  180
  200  40.0   0   360

display.txt header 传递 camera_keyframes JSON 数组,
draw.py 按帧时间线性插值驱动相机运动(循环播放)。
This commit is contained in:
2026-06-12 07:52:06 +08:00
parent f1afb7c479
commit 22b94011ee
5 changed files with 115 additions and 3 deletions
+40
View File
@@ -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])