From e40f7a49e41c6ef31b2cdc802ad3ae070274c425 Mon Sep 17 00:00:00 2001 From: Ying-Li Niu <64801511@qq.com> Date: Fri, 12 Jun 2026 07:58:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20move=5Fcamera.txt=20=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E9=80=9F=E5=BA=A6=E6=AE=B5=E6=A0=BC=E5=BC=8F=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E7=9B=B8=E6=9C=BA=E8=BF=90=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 格式: 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,实现连续平滑的相机运动。 --- compute.py | 45 ++++++++++++++----- draw.py | 65 +++++++++++++-------------- dynamics.py | 24 ++++++---- examples/case06/input/move_camera.txt | 18 ++++---- 4 files changed, 89 insertions(+), 63 deletions(-) diff --git a/compute.py b/compute.py index 49b6f69..e410988 100644 --- a/compute.py +++ b/compute.py @@ -87,25 +87,50 @@ Z_MIN = None Z_MAX = None -def _load_camera_keyframes(path): - """读取 move_camera.txt,返回 JSON 数组字符串 [[frame,dist,el,az],...] 或空串。""" +def _load_camera_motion(path): + """读取 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): print(f"[compute] 警告: 未找到 {path},跳过运动相机") return "" - keyframes = [] + segments = [] 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: + # 解析帧范围 + m = re.match(r'(\d+)\s*-\s*(\d+)', line) + if not m: + continue + 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 "" import json - return json.dumps(keyframes) + return json.dumps(segments) def _to_text_value(value): @@ -757,7 +782,7 @@ def run_from_config(config, out_dir=None): 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) + camera_keyframes_raw = _load_camera_motion(cam_path) print(f"[compute] 使用算法: {METHOD}") print(f"[compute] 已加载成键信息: {len(BOND_PAIRS)} 条键") diff --git a/draw.py b/draw.py index c8f3900..b2ff4c9 100644 --- a/draw.py +++ b/draw.py @@ -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}") -# 运动相机关键帧(可选) -_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 _interp_camera(f_idx): - """根据关键帧插值相机位置。""" - if not _CAM_KF or len(_CAM_KF) < 2: +def _update_motion_camera(f_idx): + """速度段驱动:每帧累加平移/旋转。""" + if not _CAM_MOTION: return - # 找到当前帧对应的关键帧区间 - n_kf = len(_CAM_KF) - # 映射到关键帧时间线 - total_kf_frames = _CAM_KF[-1][0] - if total_kf_frames <= 0: + global _cam_center, _cam_elev, _cam_azim, _cam_dist + # 找当前帧属于哪个段 + active = [seg for seg in _CAM_MOTION + if seg["start"] <= f_idx < seg["end"]] + if not active: 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 + seg = active[0] + _cam_center[0] += seg["v"][0] + _cam_center[1] += seg["v"][1] + _cam_center[2] += seg["v"][2] + # rx → elevation, ry → azimuth, rz → 距离变化(绕z=roll/螺旋) + _cam_elev += seg["r"][0] + _cam_azim += seg["r"][1] + _cam_dist += seg["r"][2] * 0 # rz 可以忽略或做其他用途 + + view.camera.center = tuple(_cam_center) + view.camera.distance = _cam_dist + view.camera.elevation = _cam_elev + view.camera.azimuth = _cam_azim def update(event): @@ -605,8 +600,8 @@ def update(event): if bond_lines is not None and len(BOND_PAIRS) > 0: _update_bond_positions(frame_idx) - # 运动相机:按关键帧插值 - _interp_camera(frame_idx) + # 运动相机:速度段驱动 + _update_motion_camera(frame_idx) # 信息面板显示 plot_atom 的数据 x = float(DISP_X[frame_idx]) diff --git a/dynamics.py b/dynamics.py index f391bec..6abbcc1 100644 --- a/dynamics.py +++ b/dynamics.py @@ -33,7 +33,8 @@ def _fmt_alpha(v): 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)): return "" 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) if not os.path.exists(cam_path): return "" - import json - kfs = [] + segments = [] 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 "" + m = re.match(r'(\d+)\s*-\s*(\d+)', line) + if not m: + continue + start, end = int(m.group(1)), int(m.group(2)) + 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): diff --git a/examples/case06/input/move_camera.txt b/examples/case06/input/move_camera.txt index fc2a020..ccb9fc4 100644 --- a/examples/case06/input/move_camera.txt +++ b/examples/case06/input/move_camera.txt @@ -1,11 +1,9 @@ -# move_camera.txt — 摄像机关键帧动画 -# 格式: frame distance elevation azimuth -# frame: 关键帧序号(对应动画帧,非计算步) -# distance: 到场景中心距离 -# elevation: 俯仰角(度,负值=俯视) -# azimuth: 方位角(度,沿 Y 轴顺时针旋转) +# move_camera.txt — 摄像机速度段驱动 +# 格式: start-end vx=f vy=f vz=f rx=d ry=d rz=d +# vx/vy/vz: 平移速度(每帧移动单位) +# rx/ry/rz: 旋转速度(每帧度数) +# rx → elevation(俯仰), ry → azimuth(方位), rz → (预留) # -# 两帧之间线性插值,到达最后一帧后循环 -0 40.0 0 0 -100 80.0 -30 180 -200 40.0 0 360 +# 示例:前60帧向右平移+绕x旋转,30-90帧向上平移+绕y绕z旋转 +1-60 vx=1.0 rx=10 +30-90 vy=2.0 ry=20 rz=10