feat: move_camera.txt 改为速度段格式驱动相机运动
格式: 1-60 vx=1.0 rx=10 # 1-60帧:x平移1/帧 + 绕x转10°/帧 30-90 vy=2.0 ry=20 rz=10 # 30-90帧:y平移2/帧 + 绕y转20°/帧 + 绕z转10°/帧 draw.py 每帧累加平移速度修改center,累加旋转速度修改 elevation/azimuth,实现连续平滑的相机运动。
This commit is contained in:
+35
-10
@@ -87,25 +87,50 @@ Z_MIN = None
|
|||||||
Z_MAX = None
|
Z_MAX = None
|
||||||
|
|
||||||
|
|
||||||
def _load_camera_keyframes(path):
|
def _load_camera_motion(path):
|
||||||
"""读取 move_camera.txt,返回 JSON 数组字符串 [[frame,dist,el,az],...] 或空串。"""
|
"""读取 move_camera.txt(速度段格式),返回 JSON 字符串。
|
||||||
|
|
||||||
|
格式:每行是一个运动段
|
||||||
|
start-end vx=f1 vy=f2 vz=f3 rx=d1 ry=d2 rz=d3
|
||||||
|
示例:
|
||||||
|
1-60 vx=1.0 rx=10
|
||||||
|
30-90 vy=2.0 ry=20 rz=10
|
||||||
|
|
||||||
|
返回 JSON: [{"start":N,"end":N,"v":[x,y,z],"r":[x,y,z]},...]
|
||||||
|
"""
|
||||||
|
import re
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
print(f"[compute] 警告: 未找到 {path},跳过运动相机")
|
print(f"[compute] 警告: 未找到 {path},跳过运动相机")
|
||||||
return ""
|
return ""
|
||||||
keyframes = []
|
segments = []
|
||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
parts = line.split()
|
# 解析帧范围
|
||||||
if len(parts) >= 4:
|
m = re.match(r'(\d+)\s*-\s*(\d+)', line)
|
||||||
keyframes.append([int(parts[0]), float(parts[1]),
|
if not m:
|
||||||
float(parts[2]), float(parts[3])])
|
continue
|
||||||
if not keyframes:
|
start, end = int(m.group(1)), int(m.group(2))
|
||||||
|
v = [0.0, 0.0, 0.0]
|
||||||
|
r = [0.0, 0.0, 0.0]
|
||||||
|
# 解析 vx=, vy=, vz=
|
||||||
|
for i, axis in enumerate(['x', 'y', 'z']):
|
||||||
|
m2 = re.search(r'v' + axis + r'\s*=\s*([-\d.]+)', line)
|
||||||
|
if m2:
|
||||||
|
v[i] = float(m2.group(1))
|
||||||
|
# 解析 rx=, ry=, rz=
|
||||||
|
for i, axis in enumerate(['x', 'y', 'z']):
|
||||||
|
m2 = re.search(r'r' + axis + r'\s*=\s*([-\d.]+)', line)
|
||||||
|
if m2:
|
||||||
|
r[i] = float(m2.group(1))
|
||||||
|
if any(v) or any(r):
|
||||||
|
segments.append({"start": start, "end": end, "v": v, "r": r})
|
||||||
|
if not segments:
|
||||||
return ""
|
return ""
|
||||||
import json
|
import json
|
||||||
return json.dumps(keyframes)
|
return json.dumps(segments)
|
||||||
|
|
||||||
|
|
||||||
def _to_text_value(value):
|
def _to_text_value(value):
|
||||||
@@ -757,7 +782,7 @@ def run_from_config(config, out_dir=None):
|
|||||||
cam_path = cam_rel
|
cam_path = cam_rel
|
||||||
if out_dir is not None and not os.path.isabs(cam_rel):
|
if out_dir is not None and not os.path.isabs(cam_rel):
|
||||||
cam_path = os.path.join(out_dir, cam_rel)
|
cam_path = os.path.join(out_dir, cam_rel)
|
||||||
camera_keyframes_raw = _load_camera_keyframes(cam_path)
|
camera_keyframes_raw = _load_camera_motion(cam_path)
|
||||||
|
|
||||||
print(f"[compute] 使用算法: {METHOD}")
|
print(f"[compute] 使用算法: {METHOD}")
|
||||||
print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键")
|
print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键")
|
||||||
|
|||||||
@@ -555,43 +555,38 @@ 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
|
_CAM_MOTION = json.loads(h.get("camera_keyframes", "null")) if h.get("camera_keyframes") else None
|
||||||
|
if _CAM_MOTION:
|
||||||
|
_cam_center = [0.0, 0.0, 0.0]
|
||||||
|
_cam_elev = initial_camera["elevation"]
|
||||||
|
_cam_azim = initial_camera["azimuth"]
|
||||||
|
_cam_dist = initial_camera["distance"]
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
def _update_motion_camera(f_idx):
|
||||||
# 每帧回调:仅推进帧索引,从预存数组读取位置,零物理计算
|
"""速度段驱动:每帧累加平移/旋转。"""
|
||||||
# ===========================================================================
|
if not _CAM_MOTION:
|
||||||
def _interp_camera(f_idx):
|
|
||||||
"""根据关键帧插值相机位置。"""
|
|
||||||
if not _CAM_KF or len(_CAM_KF) < 2:
|
|
||||||
return
|
return
|
||||||
# 找到当前帧对应的关键帧区间
|
global _cam_center, _cam_elev, _cam_azim, _cam_dist
|
||||||
n_kf = len(_CAM_KF)
|
# 找当前帧属于哪个段
|
||||||
# 映射到关键帧时间线
|
active = [seg for seg in _CAM_MOTION
|
||||||
total_kf_frames = _CAM_KF[-1][0]
|
if seg["start"] <= f_idx < seg["end"]]
|
||||||
if total_kf_frames <= 0:
|
if not active:
|
||||||
return
|
return
|
||||||
# 循环播放关键帧
|
seg = active[0]
|
||||||
t = (f_idx / N_FRAMES) * total_kf_frames
|
_cam_center[0] += seg["v"][0]
|
||||||
# 二分查找区间
|
_cam_center[1] += seg["v"][1]
|
||||||
lo, hi = 0, n_kf - 1
|
_cam_center[2] += seg["v"][2]
|
||||||
while hi - lo > 1:
|
# rx → elevation, ry → azimuth, rz → 距离变化(绕z=roll/螺旋)
|
||||||
mid = (lo + hi) // 2
|
_cam_elev += seg["r"][0]
|
||||||
if _CAM_KF[mid][0] <= t:
|
_cam_azim += seg["r"][1]
|
||||||
lo = mid
|
_cam_dist += seg["r"][2] * 0 # rz 可以忽略或做其他用途
|
||||||
else:
|
|
||||||
hi = mid
|
view.camera.center = tuple(_cam_center)
|
||||||
f0, f1 = _CAM_KF[lo], _CAM_KF[hi]
|
view.camera.distance = _cam_dist
|
||||||
if f1[0] - f0[0] == 0:
|
view.camera.elevation = _cam_elev
|
||||||
return
|
view.camera.azimuth = _cam_azim
|
||||||
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):
|
||||||
@@ -605,8 +600,8 @@ 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)
|
_update_motion_camera(frame_idx)
|
||||||
|
|
||||||
# 信息面板显示 plot_atom 的数据
|
# 信息面板显示 plot_atom 的数据
|
||||||
x = float(DISP_X[frame_idx])
|
x = float(DISP_X[frame_idx])
|
||||||
|
|||||||
+16
-8
@@ -33,7 +33,8 @@ def _fmt_alpha(v):
|
|||||||
|
|
||||||
|
|
||||||
def _load_camera_kf(config, runtime_base):
|
def _load_camera_kf(config, runtime_base):
|
||||||
"""加载 move_camera.txt → JSON 字符串,供 display.txt header 使用。"""
|
"""加载 move_camera.txt(速度段格式)→ JSON 字符串。"""
|
||||||
|
import re, json
|
||||||
if not int(config.get("move_camera", 0)):
|
if not int(config.get("move_camera", 0)):
|
||||||
return ""
|
return ""
|
||||||
cam_file = str(config.get("move_camera_file",
|
cam_file = str(config.get("move_camera_file",
|
||||||
@@ -43,18 +44,25 @@ def _load_camera_kf(config, runtime_base):
|
|||||||
cam_path = os.path.join(runtime_base, cam_file)
|
cam_path = os.path.join(runtime_base, cam_file)
|
||||||
if not os.path.exists(cam_path):
|
if not os.path.exists(cam_path):
|
||||||
return ""
|
return ""
|
||||||
import json
|
segments = []
|
||||||
kfs = []
|
|
||||||
with open(cam_path, "r", encoding="utf-8") as f:
|
with open(cam_path, "r", encoding="utf-8") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if not line or line.startswith("#"):
|
if not line or line.startswith("#"):
|
||||||
continue
|
continue
|
||||||
parts = line.split()
|
m = re.match(r'(\d+)\s*-\s*(\d+)', line)
|
||||||
if len(parts) >= 4:
|
if not m:
|
||||||
kfs.append([int(parts[0]), float(parts[1]),
|
continue
|
||||||
float(parts[2]), float(parts[3])])
|
start, end = int(m.group(1)), int(m.group(2))
|
||||||
return json.dumps(kfs) if kfs else ""
|
v, r = [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]
|
||||||
|
for i, axis in enumerate(['x', 'y', 'z']):
|
||||||
|
m2 = re.search(r'v' + axis + r'\s*=\s*([-\d.]+)', line)
|
||||||
|
if m2: v[i] = float(m2.group(1))
|
||||||
|
m2 = re.search(r'r' + axis + r'\s*=\s*([-\d.]+)', line)
|
||||||
|
if m2: r[i] = float(m2.group(1))
|
||||||
|
if any(v) or any(r):
|
||||||
|
segments.append({"start": start, "end": end, "v": v, "r": r})
|
||||||
|
return json.dumps(segments) if segments else ""
|
||||||
|
|
||||||
|
|
||||||
def read_optional_index(data, key, default_value):
|
def read_optional_index(data, key, default_value):
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# move_camera.txt — 摄像机关键帧动画
|
# move_camera.txt — 摄像机速度段驱动
|
||||||
# 格式: frame distance elevation azimuth
|
# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d
|
||||||
# frame: 关键帧序号(对应动画帧,非计算步)
|
# vx/vy/vz: 平移速度(每帧移动单位)
|
||||||
# distance: 到场景中心距离
|
# rx/ry/rz: 旋转速度(每帧度数)
|
||||||
# elevation: 俯仰角(度,负值=俯视)
|
# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留)
|
||||||
# azimuth: 方位角(度,沿 Y 轴顺时针旋转)
|
|
||||||
#
|
#
|
||||||
# 两帧之间线性插值,到达最后一帧后循环
|
# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转
|
||||||
0 40.0 0 0
|
1-60 vx=1.0 rx=10
|
||||||
100 80.0 -30 180
|
30-90 vy=2.0 ry=20 rz=10
|
||||||
200 40.0 0 360
|
|
||||||
|
|||||||
Reference in New Issue
Block a user