feat: 增加驱动力系统、Marker渲染模式、动画防闪退、案例文档

- 新增 driving_force 驱动力系统(driver.txt 定义,支持周期控制)
- 新增 use_marker 渲染开关(GPU实例化点精灵,提升大量原子性能)
- 修复动画闪退:独立控制台、错误日志、启动存活检测
- 重绘 draw.py 架构:双渲染模式 + 预分配键线缓冲区
- 修复 raw trajectory 采样时间变量遮蔽 bug
- 重构 case05: 60原子一维链 + 驱动力 + 完整案例文档
- 修复所有案例 Readme.md 编码(GBK → UTF-8)
- 所有 input.txt 新增 driver_file / driving_force / use_marker 参数
This commit is contained in:
2026-06-10 15:34:53 +08:00
parent 0f04630fc0
commit 854f00ae44
28 changed files with 1404 additions and 68 deletions
+73 -46
View File
@@ -72,6 +72,9 @@ PLOT_ATOM_ROW = int(disp_data.get("plot_atom_row", 0))
PLOT_ATOM_ID = int(disp_data.get("plot_atom_id", ATOM_IDS[0]))
BOND_PAIRS = disp_data.get("bond_pairs", [])
# 渲染方式:0=Sphere(网格球体), 1=Marker(GPU点精灵)
USE_MARKER = int(disp_data.get("use_marker", 0))
if N_FRAMES <= 0:
raise ValueError(
"output/display.txt 中没有可播放的帧,请检查 sample_start/sample_end/NSTEP 配置。")
@@ -171,7 +174,10 @@ axes_group.append(scene.visuals.Text(text="y", color=(0.2, 1.0, 0.2, 1.0), font_
axes_group.append(scene.visuals.Text(text="z", color=(0.3, 0.6, 1.0, 1.0), font_size=18,
pos=(0, 0, axis_length + 0.2), anchor_x="left", anchor_y="bottom", parent=view.scene))
# 所有小球(每个原子一个球,不同颜色)
# ── 原子渲染 ──────────────────────────────────
# 两种渲染模式:
# Sphere = 独立网格球体(精细,适合少量原子)
# Marker = GPU 实例化点精灵(高效,适合大量原子)
TAB10_RGB = np.array([
[0.1216, 0.4667, 0.7059], # 蓝
[0.8902, 0.4667, 0.1137], # 橙
@@ -184,23 +190,41 @@ TAB10_RGB = np.array([
[0.7373, 0.7412, 0.1333], # 黄绿
[0.0902, 0.7451, 0.8118], # 青
])
balls = []
# 每个原子的颜色(循环使用 tab10 色板)
atom_colors = np.zeros((N_ATOMS, 4), dtype=np.float32)
for i in range(N_ATOMS):
r, g, b = TAB10_RGB[i % len(TAB10_RGB)]
s = scene.visuals.Sphere(
radius=float(ATOM_RADII[i]), method="latitude",
color=(r, g, b, 1.0), edge_color=None, parent=view.scene)
s.mesh.shading = "smooth"
balls.append(s)
atom_colors[i] = [r, g, b, 1.0]
if USE_MARKER:
# ── Marker 模式:GPU 实例化,一次 draw call ──
marker_pos = np.zeros((N_ATOMS, 3), dtype=np.float32)
marker_sizes = np.array([float(ATOM_RADII[i]) * 40 for i in range(N_ATOMS)], dtype=np.float32)
balls = scene.visuals.Markers(parent=view.scene)
balls.set_data(
pos=marker_pos, face_color=atom_colors,
size=marker_sizes, symbol='disc', edge_width=0,
)
else:
# ── Sphere 模式:每个原子一个独立网格球体 ──
balls = []
for i in range(N_ATOMS):
r, g, b, _ = atom_colors[i]
s = scene.visuals.Sphere(
radius=float(ATOM_RADII[i]), method="latitude",
color=(r, g, b, 1.0), edge_color=None, parent=view.scene)
s.mesh.shading = "smooth"
balls.append(s)
# 成键线(原子之间的连接)
if len(BOND_PAIRS) > 0:
n_bonds = len(BOND_PAIRS)
bond_pos = np.zeros((n_bonds * 2, 3), dtype=np.float32)
bond_pos_buffer = np.zeros((n_bonds * 2, 3), dtype=np.float32) # 预分配,后续原地修改
bond_lines = scene.visuals.Line(
pos=bond_pos, color=(0.7, 0.7, 0.7, 0.8), width=3,
pos=bond_pos_buffer, color=(0.7, 0.7, 0.7, 0.8), width=3,
connect="segments", parent=view.scene)
else:
bond_pos_buffer = None
bond_lines = None
# 六个面形成立方体边界(每个面独立透明度,alpha<=0 时隐藏该面)
@@ -361,19 +385,10 @@ def handle_key_press(event):
def reset_camera_view():
global frame_idx
frame_idx = 0
# 立即复位所有小球到第 0 帧
for i in range(N_ATOMS):
balls[i].transform = STTransform(translate=(
float(DISP_ALL_X[frame_idx, i]),
float(DISP_ALL_Y[frame_idx, i]),
float(DISP_ALL_Z[frame_idx, i]),
))
# 立即复位所有原子到第 0 帧
_update_atom_positions(frame_idx)
if bond_lines is not None and len(BOND_PAIRS) > 0:
pos = np.zeros((len(BOND_PAIRS) * 2, 3), dtype=np.float32)
for b_idx, (i, j) in enumerate(BOND_PAIRS):
pos[b_idx * 2] = [DISP_ALL_X[frame_idx, i], DISP_ALL_Y[frame_idx, i], DISP_ALL_Z[frame_idx, i]]
pos[b_idx * 2 + 1] = [DISP_ALL_X[frame_idx, j], DISP_ALL_Y[frame_idx, j], DISP_ALL_Z[frame_idx, j]]
bond_lines.set_data(pos=pos)
_update_bond_positions(frame_idx)
# 复位相机
view.camera.distance = initial_camera["distance"]
view.camera.elevation = initial_camera["elevation"]
@@ -413,31 +428,51 @@ def handle_mouse_press(event):
axis.visible = axes_visible
# ===========================================================================
# 位置/键线更新辅助函数(两种渲染模式共用)
# ===========================================================================
def _update_atom_positions(f_idx):
"""更新所有原子到第 f_idx 帧的位置。"""
if USE_MARKER:
for i in range(N_ATOMS):
marker_pos[i] = [DISP_ALL_X[f_idx, i], DISP_ALL_Y[f_idx, i], DISP_ALL_Z[f_idx, i]]
balls.set_data(pos=marker_pos)
else:
for i in range(N_ATOMS):
balls[i].transform = STTransform(translate=(
float(DISP_ALL_X[f_idx, i]),
float(DISP_ALL_Y[f_idx, i]),
float(DISP_ALL_Z[f_idx, i]),
))
def _update_bond_positions(f_idx):
"""原地更新键线顶点位置,避免重新分配内存。"""
for b_idx, (i, j) in enumerate(BOND_PAIRS):
b2 = b_idx * 2
bond_pos_buffer[b2] = [DISP_ALL_X[f_idx, i], DISP_ALL_Y[f_idx, i], DISP_ALL_Z[f_idx, i]]
bond_pos_buffer[b2 + 1] = [DISP_ALL_X[f_idx, j], DISP_ALL_Y[f_idx, j], DISP_ALL_Z[f_idx, j]]
bond_lines.set_data(pos=bond_pos_buffer)
# ===========================================================================
# 动画初始化
# ===========================================================================
frame_idx = 0
# 初始帧:摆放所有小球并刷新 UI
for i in range(N_ATOMS):
balls[i].transform = STTransform(translate=(
float(DISP_ALL_X[frame_idx, i]),
float(DISP_ALL_Y[frame_idx, i]),
float(DISP_ALL_Z[frame_idx, i]),
))
# 初始帧:更新成键线
# 初始帧:摆放所有原子并刷新 UI
_update_atom_positions(frame_idx)
if bond_lines is not None and len(BOND_PAIRS) > 0:
pos = np.zeros((len(BOND_PAIRS) * 2, 3), dtype=np.float32)
for b_idx, (i, j) in enumerate(BOND_PAIRS):
pos[b_idx * 2] = [DISP_ALL_X[frame_idx, i], DISP_ALL_Y[frame_idx, i], DISP_ALL_Z[frame_idx, i]]
pos[b_idx * 2 + 1] = [DISP_ALL_X[frame_idx, j], DISP_ALL_Y[frame_idx, j], DISP_ALL_Z[frame_idx, j]]
bond_lines.set_data(pos=pos)
_update_bond_positions(frame_idx)
reposition_camera_info()
update_ball_info(frame_idx,
float(DISP_X[frame_idx]), float(DISP_Y[frame_idx]), float(DISP_Z[frame_idx]),
float(DISP_VX[frame_idx]), float(DISP_VY[frame_idx]), float(DISP_VZ[frame_idx]))
mode_str = "Marker (GPU 实例化)" if USE_MARKER else "Sphere (独立网格)"
print(f"[draw] 加载 output/display.txt: {N_FRAMES} 帧, {N_ATOMS} 个原子, NT={NT}, DT={DT}, NSTEP={NSTEP}")
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}")
@@ -448,20 +483,12 @@ def update(event):
global frame_idx
frame_idx = (frame_idx + 1) % N_FRAMES # 循环播放
# 更新所有小球位置
for i in range(N_ATOMS):
x = float(DISP_ALL_X[frame_idx, i])
y = float(DISP_ALL_Y[frame_idx, i])
z = float(DISP_ALL_Z[frame_idx, i])
balls[i].transform = STTransform(translate=(x, y, z))
# 更新所有原子位置(两种模式共用逻辑)
_update_atom_positions(frame_idx)
# 更新成键线
# 更新成键线(原地修改,无内存分配)
if bond_lines is not None and len(BOND_PAIRS) > 0:
pos = np.zeros((len(BOND_PAIRS) * 2, 3), dtype=np.float32)
for b_idx, (i, j) in enumerate(BOND_PAIRS):
pos[b_idx * 2] = [DISP_ALL_X[frame_idx, i], DISP_ALL_Y[frame_idx, i], DISP_ALL_Z[frame_idx, i]]
pos[b_idx * 2 + 1] = [DISP_ALL_X[frame_idx, j], DISP_ALL_Y[frame_idx, j], DISP_ALL_Z[frame_idx, j]]
bond_lines.set_data(pos=pos)
_update_bond_positions(frame_idx)
# 信息面板显示 plot_atom 的数据
x = float(DISP_X[frame_idx])