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:
+36
-2
@@ -65,6 +65,7 @@ box_color_r = None
|
|||||||
box_color_g = None
|
box_color_g = None
|
||||||
box_color_b = None
|
box_color_b = None
|
||||||
use_marker = 0
|
use_marker = 0
|
||||||
|
camera_keyframes_raw = ""
|
||||||
|
|
||||||
# 力开关
|
# 力开关
|
||||||
GRAVITY_FIELD = 1 # 均匀重力场
|
GRAVITY_FIELD = 1 # 均匀重力场
|
||||||
@@ -86,6 +87,27 @@ Z_MIN = None
|
|||||||
Z_MAX = 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):
|
def _to_text_value(value):
|
||||||
"""Convert numpy-heavy objects into JSON-friendly plain Python values."""
|
"""Convert numpy-heavy objects into JSON-friendly plain Python values."""
|
||||||
if isinstance(value, np.ndarray):
|
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 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 ball_radius, ball_color_r, ball_color_g, ball_color_b
|
||||||
global box_color_r, box_color_g, box_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 warmup_steps, sample_start, sample_end
|
||||||
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
|
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
|
||||||
global DRIVING_FORCE, DRIVER_DATA
|
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_path = os.path.join(out_dir, driver_rel)
|
||||||
DRIVER_DATA = load_driver_file(driver_path, ATOM_IDS)
|
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] 使用算法: {METHOD}")
|
||||||
print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键")
|
print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键")
|
||||||
if config.get("_skip_run", False):
|
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),
|
"atom_radii": ",".join(str(r) for r in ATOM_RADII),
|
||||||
"camera_distance": str(config.get("camera_distance", 40.0)),
|
"camera_distance": str(config.get("camera_distance", 40.0)),
|
||||||
"camera_elevation": str(config.get("camera_elevation", 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} 帧)")
|
print(f"[compute] display.txt 已保存至: {disp_path} ({n_frames_actual} 帧)")
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from vispy import app, scene
|
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}")
|
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):
|
def update(event):
|
||||||
global frame_idx
|
global frame_idx
|
||||||
frame_idx = (frame_idx + 1) % N_FRAMES # 循环播放
|
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:
|
if bond_lines is not None and len(BOND_PAIRS) > 0:
|
||||||
_update_bond_positions(frame_idx)
|
_update_bond_positions(frame_idx)
|
||||||
|
|
||||||
|
# 运动相机:按关键帧插值
|
||||||
|
_interp_camera(frame_idx)
|
||||||
|
|
||||||
# 信息面板显示 plot_atom 的数据
|
# 信息面板显示 plot_atom 的数据
|
||||||
x = float(DISP_X[frame_idx])
|
x = float(DISP_X[frame_idx])
|
||||||
y = float(DISP_Y[frame_idx])
|
y = float(DISP_Y[frame_idx])
|
||||||
|
|||||||
+27
-1
@@ -32,6 +32,31 @@ def _fmt_alpha(v):
|
|||||||
return str(float(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):
|
def read_optional_index(data, key, default_value):
|
||||||
"""Read an optional integer index from structured txt metadata."""
|
"""Read an optional integer index from structured txt metadata."""
|
||||||
if key not in data:
|
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", [])),
|
"atom_radii": _fmt_alpha(data.get("atom_radii", [])),
|
||||||
"camera_distance": str(config.get("camera_distance", 40.0)),
|
"camera_distance": str(config.get("camera_distance", 40.0)),
|
||||||
"camera_elevation": str(config.get("camera_elevation", 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)
|
n_frames = len(indices)
|
||||||
compute.save_display_txt(
|
compute.save_display_txt(
|
||||||
|
|||||||
@@ -105,3 +105,4 @@ box_color_b: 0.85
|
|||||||
camera_distance: 40.0 # 摄像机到场景中心的距离
|
camera_distance: 40.0 # 摄像机到场景中心的距离
|
||||||
camera_elevation: 0 # 俯仰角(度),负值=俯视
|
camera_elevation: 0 # 俯仰角(度),负值=俯视
|
||||||
camera_azimuth: 0 # 方位角(度)
|
camera_azimuth: 0 # 方位角(度)
|
||||||
|
move_camera: 0 # 0=固定视角, 1=按 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
|
||||||
Reference in New Issue
Block a user