feat: add display_amp parameter for visual displacement amplification
Supports display_amp: [ax, ay, az] in input.txt. On rendering, each atom's displacement from its frame-0 equilibrium is multiplied by the corresponding factor. Physics is unchanged; only the rendered positions are scaled. Useful for visualizing small-amplitude waves that would otherwise be invisible. Example: display_amp: [1.0, 1.0, 5.0] exaggerates z-direction motion 5x. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+15
-3
@@ -75,6 +75,7 @@ camera_center_z = 0.0
|
||||
camera_pos_x = None
|
||||
camera_pos_y = None
|
||||
camera_pos_z = None
|
||||
display_amp_str = ""
|
||||
FIXED_MASK_X = None
|
||||
FIXED_MASK_Y = None
|
||||
FIXED_MASK_Z = None
|
||||
@@ -774,7 +775,7 @@ def run_from_config(config, out_dir=None):
|
||||
global box_color_r, box_color_g, box_color_b
|
||||
global use_marker, camera_keyframes_raw, camera_distance, camera_elevation, camera_azimuth
|
||||
global camera_center_x, camera_center_y, camera_center_z
|
||||
global camera_pos_x, camera_pos_y, camera_pos_z
|
||||
global camera_pos_x, camera_pos_y, camera_pos_z, display_amp_str
|
||||
global FIXED_MASK_X, FIXED_MASK_Y, FIXED_MASK_Z
|
||||
global warmup_steps, sample_start, sample_end
|
||||
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
|
||||
@@ -849,6 +850,13 @@ def run_from_config(config, out_dir=None):
|
||||
camera_pos_x = config.get("camera_pos_x", None)
|
||||
camera_pos_y = config.get("camera_pos_y", None)
|
||||
camera_pos_z = config.get("camera_pos_z", None)
|
||||
_damp_cfg = config.get("display_amp", None)
|
||||
if _damp_cfg is not None:
|
||||
import ast as _ast_cfg
|
||||
_dv = np.array(_ast_cfg.literal_eval(str(_damp_cfg).strip()), dtype=np.float64)
|
||||
display_amp_str = f"[{_dv[0]},{_dv[1]},{_dv[2]}]"
|
||||
else:
|
||||
display_amp_str = ""
|
||||
|
||||
# 力开关
|
||||
global GRAVITY_FIELD, GRAVITY_INTERACTION, ELASTIC_FORCE, DAMPING_FORCE, GRAVITY_STRENGTH
|
||||
@@ -1179,6 +1187,8 @@ def run_engine(engine, input_dir, output_dir, config):
|
||||
_cam_extra["camera_pos_x"] = str(camera_pos_x)
|
||||
_cam_extra["camera_pos_y"] = str(camera_pos_y)
|
||||
_cam_extra["camera_pos_z"] = str(camera_pos_z)
|
||||
if display_amp_str:
|
||||
_cam_extra["display_amp"] = display_amp_str
|
||||
save_display_npz(
|
||||
_npz_path,
|
||||
_d["frames_x"], _d["frames_y"], _d["frames_z"],
|
||||
@@ -1779,7 +1789,8 @@ def run_simulation(save_trajectory=0):
|
||||
"camera_pos_y": str(camera_pos_y),
|
||||
"camera_pos_z": str(camera_pos_z),
|
||||
} if camera_pos_x is not None else {}),
|
||||
"camera_keyframes": str(camera_keyframes_raw)}
|
||||
"camera_keyframes": str(camera_keyframes_raw),
|
||||
**({"display_amp": display_amp_str} if display_amp_str else {})}
|
||||
)
|
||||
print(f"[compute] display.txt 已保存至: {disp_path} ({n_frames_actual} 帧)")
|
||||
|
||||
@@ -1822,7 +1833,8 @@ def run_simulation(save_trajectory=0):
|
||||
"camera_center_z": str(camera_center_z),
|
||||
"camera_keyframes": str(camera_keyframes_raw),
|
||||
"number_of_frames": str(record_steps),
|
||||
"number_of_particles": str(n_atoms)}
|
||||
"number_of_particles": str(n_atoms),
|
||||
**({"display_amp": display_amp_str} if display_amp_str else {})}
|
||||
save_display_npz(_npz_path,
|
||||
sampled_x[:n_frames_actual], sampled_y[:n_frames_actual],
|
||||
sampled_z[:n_frames_actual],
|
||||
|
||||
@@ -67,6 +67,20 @@ N_FRAMES = DISP_ALL_X.shape[0]
|
||||
NT = int(disp_data["n_total_frames"])
|
||||
N_ATOMS = int(disp_data["n_total_particles"])
|
||||
DT = float(h.get("DT", 0.001))
|
||||
|
||||
# 视觉位移放大:display_amp: [ax, ay, az],对偏离第0帧的位移乘以倍数
|
||||
_damp_raw = h.get("display_amp", "")
|
||||
if _damp_raw.strip():
|
||||
import ast as _ast
|
||||
_damp_vals = _ast.literal_eval(_damp_raw.strip())
|
||||
_damp = np.array(_damp_vals, dtype=np.float64)
|
||||
if _damp.shape == (3,) and not np.allclose(_damp, 1.0):
|
||||
_eq_x = DISP_ALL_X[0:1, :] # 第0帧作为平衡位置参考
|
||||
_eq_y = DISP_ALL_Y[0:1, :]
|
||||
_eq_z = DISP_ALL_Z[0:1, :]
|
||||
DISP_ALL_X = _eq_x + (DISP_ALL_X - _eq_x) * _damp[0]
|
||||
DISP_ALL_Y = _eq_y + (DISP_ALL_Y - _eq_y) * _damp[1]
|
||||
DISP_ALL_Z = _eq_z + (DISP_ALL_Z - _eq_z) * _damp[2]
|
||||
NSTEP = int(h.get("NSTEP", 1))
|
||||
DISP_STEP = np.arange(N_FRAMES) * NSTEP
|
||||
DISP_T = DISP_STEP * DT
|
||||
@@ -119,11 +133,31 @@ box_color_b = float(h.get("box_color_b", 0.85))
|
||||
info_margin = 8
|
||||
axis_length = 10.0
|
||||
|
||||
import math as _math_cam
|
||||
|
||||
_cx = float(h.get("camera_center_x", 0.0))
|
||||
_cy = float(h.get("camera_center_y", 0.0))
|
||||
_cz = float(h.get("camera_center_z", 0.0))
|
||||
|
||||
# 若 input.txt 指定了摄像机自身坐标,则由坐标反推 distance/elevation/azimuth
|
||||
if h.get("camera_pos_x") is not None:
|
||||
_px = float(h["camera_pos_x"])
|
||||
_py = float(h["camera_pos_y"])
|
||||
_pz = float(h["camera_pos_z"])
|
||||
_dx, _dy, _dz = _px - _cx, _py - _cy, _pz - _cz
|
||||
_dist = _math_cam.sqrt(_dx*_dx + _dy*_dy + _dz*_dz) or 1.0
|
||||
_elev = _math_cam.degrees(_math_cam.asin(max(-1.0, min(1.0, _dy / _dist))))
|
||||
_azim = _math_cam.degrees(_math_cam.atan2(_dx, _dz))
|
||||
else:
|
||||
_dist = float(h.get("camera_distance", 40.0))
|
||||
_elev = float(h.get("camera_elevation", 0))
|
||||
_azim = float(h.get("camera_azimuth", 0))
|
||||
|
||||
initial_camera = {
|
||||
"distance": float(h.get("camera_distance", 40.0)),
|
||||
"elevation": float(h.get("camera_elevation", 0)),
|
||||
"azimuth": float(h.get("camera_azimuth", 0)),
|
||||
"center": (0, 0, 0),
|
||||
"distance": _dist,
|
||||
"elevation": _elev,
|
||||
"azimuth": _azim,
|
||||
"center": (_cx, _cy, _cz),
|
||||
}
|
||||
|
||||
|
||||
@@ -595,7 +629,9 @@ def _load_move_camera_txt():
|
||||
return segs if segs else None
|
||||
|
||||
# 先试 move_camera.txt 直读,没有则用 display.txt 缓存
|
||||
_CAM_MOTION = _load_move_camera_txt()
|
||||
# header 中 camera_keyframes 为空字符串表示 move_camera=0(开关关闭),跳过文件加载
|
||||
_camera_motion_enabled = bool(h.get("camera_keyframes", ""))
|
||||
_CAM_MOTION = _load_move_camera_txt() if _camera_motion_enabled else None
|
||||
if not _CAM_MOTION:
|
||||
_CAM_MOTION = json.loads(h.get("camera_keyframes", "null")) if h.get("camera_keyframes") else None
|
||||
if _CAM_MOTION:
|
||||
@@ -611,12 +647,12 @@ def _update_motion_camera(f_idx):
|
||||
"""速度段驱动:每帧累加平移/旋转。
|
||||
|
||||
时间交叠时所有段同时生效,按文件中出现的顺序依次作用。
|
||||
矩阵操作不具有对易性,排在前面的段优先作用于相机位置。
|
||||
只有当前帧存在活动段时才覆写相机,否则保留用户键盘/鼠标操作的结果。
|
||||
"""
|
||||
if not _CAM_MOTION:
|
||||
return
|
||||
global _cam_center, _cam_elev, _cam_azim, _cam_dist
|
||||
# 找当前帧所有活动的段(时间交叠=同时作用),按文件顺序依次应用
|
||||
active = False
|
||||
for seg in _CAM_MOTION:
|
||||
if seg["start"] <= f_idx < seg["end"]:
|
||||
_cam_center[0] += seg["v"][0]
|
||||
@@ -624,8 +660,9 @@ def _update_motion_camera(f_idx):
|
||||
_cam_center[2] += seg["v"][2]
|
||||
_cam_elev += seg["r"][0]
|
||||
_cam_azim += seg["r"][1]
|
||||
# rz 预留
|
||||
active = True
|
||||
|
||||
if active:
|
||||
view.camera.center = tuple(_cam_center)
|
||||
view.camera.distance = _cam_dist
|
||||
view.camera.elevation = _cam_elev
|
||||
|
||||
Reference in New Issue
Block a user