glWidget_simple.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. import sys
  2. import numpy as np
  3. from PyQt5.QtGui import QColor, QFont
  4. from PyQt5.QtWidgets import QApplication, QOpenGLWidget, QVBoxLayout, QWidget
  5. from PyQt5.QtCore import Qt, pyqtSignal
  6. from OpenGL.GL import *
  7. from OpenGL.GLU import *
  8. from OpenGL.GLUT import glutBitmapCharacter, glutStrokeCharacter, GLUT_BITMAP_HELVETICA_18, GLUT_BITMAP_TIMES_ROMAN_24 # GLUT_BITMAP_HELVETICA_18是运行时调用,标红正常
  9. class Simple3DWidget(QOpenGLWidget):
  10. pointPicked = pyqtSignal(float, float, float)
  11. def __init__(self, parent=None):
  12. super().__init__(parent)
  13. self.vertices = np.array([]) # 顶点数据
  14. self.colors = np.array([]) # 颜色数据
  15. self.triangles = np.array([], dtype=np.int32) # 三角面数据 ← 新增
  16. self.normals = np.array([]) # 法线数据 ← 新增
  17. self.edges = np.array([]) #轮廓边
  18. self.face_normals = np.array([])
  19. self.rotation = [0.0, 0.0] # 旋转角度 [俯仰, 偏航]
  20. self.zoom = -5.0 # 视距(负值表示拉远)
  21. self.pan = [0.0, 0.0] # 平移偏移 [x, y]
  22. self.last_mouse_pos = None # 鼠标位置
  23. self.setMouseTracking(True) # 启用鼠标跟踪
  24. self.display_mode = 'surface' # 默认显示模式: 点云
  25. self.axes_display_mode = False
  26. self.axes_world_display_mode = True
  27. # 🔧 模型变换
  28. self.model_rotation = [0.0, 0.0] # 模型的 [俯仰, 偏航]
  29. self.model_pan = [0.0, 0.0] # 模型平移 (x, y)
  30. self.model_scale = 1.0 # 模型缩放
  31. # 🎯 视角(世界)变换
  32. self.view_rotation = [0.0, 0.0] # 模型的 [俯仰, 偏航]
  33. self.view_pan = [0.0, 0.0] # 视点平移 (X, Y),用于 Ctrl+右键
  34. self.view_distance = 8.0 # 视点到目标的距离
  35. #选点模式
  36. self.selected_point = None # 存储选中的点坐标
  37. self.picking = False # 是否处于拾取模式
  38. self.picking_color_map = {} # 顶点索引 → 唯一颜色(用于反查)
  39. #选色模式
  40. self.set_color = False
  41. self.highlighted_face_indices = [] # 存储所有要高亮的面索引
  42. self.setFont(QFont("SimHei", 10)) # 让 renderText 使用黑体
  43. def initializeGL(self):
  44. """初始化 OpenGL 状态"""
  45. print("✅ 3D OpenGL 初始化")
  46. glEnable(GL_DEPTH_TEST)
  47. glEnable(GL_COLOR_MATERIAL)
  48. glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
  49. glEnable(GL_LIGHT0)
  50. glLightfv(GL_LIGHT0, GL_POSITION, (1.0, 1.0, 1.0, 0.0)) # 平行光
  51. glClearColor(0.1, 0.1, 0.1, 1.0)
  52. def resizeGL(self, width, height):
  53. """窗口大小改变时调用"""
  54. print(f"✅ 调整大小: {width}x{height}")
  55. if height == 0:
  56. height = 1
  57. glViewport(0, 0, width, height)
  58. self.update() # 重新绘制
  59. def paintGL(self):
  60. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  61. glEnable(GL_DEPTH_TEST)
  62. glMatrixMode(GL_PROJECTION)
  63. glLoadIdentity()
  64. gluPerspective(45, self.width() / max(self.height(), 1), 0.1, 100.0)
  65. glMatrixMode(GL_MODELVIEW)
  66. glLoadIdentity()
  67. # --- 设置摄像机 ---
  68. yaw = self.view_rotation[1]
  69. pitch = self.view_rotation[0]
  70. distance = self.view_distance
  71. cam_x = distance * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch))
  72. cam_y = distance * np.sin(np.radians(pitch))
  73. cam_z = distance * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch))
  74. gluLookAt(
  75. cam_x, cam_y, cam_z,
  76. 0.0, 0.0, 0.0,
  77. 0.0, 1.0, 0.0
  78. )
  79. glTranslatef(self.view_pan[0], self.view_pan[1], 0)
  80. # 绘制世界坐标轴
  81. if self.axes_world_display_mode:
  82. self.drawWorldAxes()
  83. # --- 模型变换 ---
  84. glPushMatrix()
  85. glTranslatef(self.model_pan[0], self.model_pan[1], 0)
  86. glRotatef(self.model_rotation[0], 1, 0, 0)
  87. glRotatef(self.model_rotation[1], 0, 1, 0)
  88. glScalef(self.model_scale, self.model_scale, self.model_scale)
  89. # 所有轮廓线已在 load_data_from_file 中提取为 self.edges
  90. # 绘制模型坐标轴
  91. if self.axes_display_mode:
  92. self.drawModelAxes()
  93. self._draw_model()
  94. glPopMatrix()
  95. def _draw_model(self):
  96. """私有方法:绘制模型(含高亮 + 静态轮廓线)"""
  97. if len(self.vertices) == 0:
  98. return
  99. if self.display_mode == 'points':
  100. # ✅ 点云模式:只显示顶点
  101. glDisable(GL_LIGHTING)
  102. glPointSize(5.0)
  103. glBegin(GL_POINTS)
  104. for i, v in enumerate(self.vertices):
  105. if self.selected_point is not None and np.allclose(v, self.selected_point, atol=1e-6):
  106. glColor3f(1.0, 1.0, 0.0) # 黄色高亮
  107. else:
  108. if i < len(self.colors):
  109. color = self.colors[i]
  110. glColor3f(color[0], color[1], color[2])
  111. else:
  112. glColor3f(0.8, 0.8, 0.8)
  113. glVertex3f(v[0], v[1], v[2])
  114. glEnd()
  115. # 额外高亮选中点
  116. if self.selected_point is not None:
  117. glPointSize(12.0)
  118. glBegin(GL_POINTS)
  119. glColor3f(1.0, 1.0, 0.0)
  120. glVertex3f(self.selected_point[0], self.selected_point[1], self.selected_point[2])
  121. glEnd()
  122. elif self.display_mode == 'surface' and len(self.triangles) > 0:
  123. # ✅ 开启光照
  124. glEnable(GL_LIGHTING)
  125. try:
  126. highlight_indices = set()
  127. if self.selected_point is not None:
  128. for idx, v in enumerate(self.vertices):
  129. if np.allclose(v, self.selected_point, atol=1e-6):
  130. highlight_indices.add(idx)
  131. # === 绘制表面 ===
  132. glBegin(GL_TRIANGLES)
  133. try:
  134. # 将 triangles 转为 list of tuples 便于索引查找(只做一次)
  135. # 如果 self.triangles 是 numpy 数组,转换为列表
  136. triangles_list = [tuple(tri) for tri in self.triangles] if isinstance(self.triangles,
  137. np.ndarray) else self.triangles
  138. for tri_idx, tri in enumerate(self.triangles):
  139. # 判断当前三角形是否在高亮区域
  140. is_face_highlighted = (
  141. hasattr(self, 'highlighted_face_indices') and
  142. isinstance(self.highlighted_face_indices, (list, set)) and
  143. tri_idx in self.highlighted_face_indices
  144. )
  145. for idx in tri:
  146. # 优先级:共面高亮 > 选中点高亮 > 正常颜色
  147. if is_face_highlighted:
  148. glColor3f(1.0, 1.0, 1.0) # 白色高亮(共面区域)
  149. elif idx in highlight_indices:
  150. glColor3f(1.0, 0.0, 0.0) # 红色高亮(选中点)
  151. else:
  152. glColor3f(*self.colors[idx])
  153. if len(self.normals) > idx:
  154. n = self.normals[idx]
  155. if not np.any(np.isnan(n)) and not np.any(np.isinf(n)):
  156. glNormal3f(*n)
  157. glVertex3f(*self.vertices[idx])
  158. except Exception as e:
  159. print(f"Error in drawing triangles: {e}")
  160. finally:
  161. glEnd()
  162. # === 绘制轮廓线(静态,来自 self.edges)===
  163. glDisable(GL_LIGHTING)
  164. if len(self.edges) > 0:
  165. glLineWidth(2.5)
  166. glColor3f(0.0, 0.0, 0.0) # 黑色轮廓线
  167. glBegin(GL_LINES)
  168. try:
  169. for edge in self.edges:
  170. v0 = self.vertices[edge[0]]
  171. v1 = self.vertices[edge[1]]
  172. glVertex3f(v0[0], v0[1], v0[2])
  173. glVertex3f(v1[0], v1[1], v1[2])
  174. finally:
  175. glEnd()
  176. glEnable(GL_LIGHTING)
  177. finally:
  178. glDisable(GL_LIGHTING)
  179. def renderText(self, x, y, z, text):
  180. """
  181. 在指定的三维坐标位置渲染文本。
  182. :param x: X轴坐标
  183. :param y: Y轴坐标
  184. :param z: Z轴坐标
  185. :param text: 要渲染的文本内容
  186. """
  187. glRasterPos3f(x, y, z) # 设置文本位置
  188. for ch in text:
  189. glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, ord(ch)) # 渲染每个字符
  190. def drawWorldAxes(self):
  191. """绘制固定的世界坐标系(左下角)"""
  192. glPushMatrix()
  193. try:
  194. # 移动到左下角
  195. glTranslatef(-4.0, -4.0, -5.0)
  196. glLineWidth(2.0)
  197. glBegin(GL_LINES)
  198. # X (红)
  199. glColor3f(1, 0, 0)
  200. glVertex3f(0, 0, 0)
  201. glVertex3f(1000, 0, 0)
  202. # Y (绿)
  203. glColor3f(0, 1, 0)
  204. glVertex3f(0, 0, 0)
  205. glVertex3f(0, 1000, 0)
  206. # Z (蓝)
  207. glColor3f(0, 0, 1)
  208. glVertex3f(0, 0, 0)
  209. glVertex3f(0, 0, 1000)
  210. glEnd() # 结束绘制
  211. # ✅ 确保 glEnd() 后再绘制文本,避免 OpenGL 状态混乱
  212. # 绘制文本标签
  213. glColor3f(1.0, 0.0, 0.0)
  214. self.renderText(1.5, 0, 0, 'X')
  215. glColor3f(0.0, 1.0, 0.0)
  216. self.renderText(0, 1.5, 0, 'Y')
  217. glColor3f(0.0, 0.0, 1.0)
  218. self.renderText(0, 0, 1.5, 'Z')
  219. finally:
  220. # ✅ 无论是否出错,都确保弹出矩阵栈
  221. glPopMatrix()
  222. def drawModelAxes(self):
  223. """绘制随模型移动的坐标系"""
  224. glPushMatrix()
  225. glLineWidth(2.5)
  226. glBegin(GL_LINES)
  227. # X
  228. glColor3f(1, 0, 0)
  229. glVertex3f(0, 0, 0);
  230. glVertex3f(2, 0, 0)
  231. # Y
  232. glColor3f(0, 1, 0)
  233. glVertex3f(0, 0, 0);
  234. glVertex3f(0, 2, 0)
  235. # Z
  236. glColor3f(0, 0, 1)
  237. glVertex3f(0, 0, 0);
  238. glVertex3f(0, 0, 2)
  239. glEnd()
  240. # 绘制文本标签
  241. glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色
  242. self.renderText(1.5, 0, 0, 'X') # X轴标签
  243. glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色
  244. self.renderText(0, 1.5, 0, 'Y') # Y轴标签
  245. glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色
  246. self.renderText(0, 0, 1.5, 'Z') # Z轴标签
  247. glPopMatrix()
  248. def set_data(self, vertices, colors, triangles=None, normals=None, silhouette_edges=None):
  249. """设置 3D 数据(支持 mesh 和轮廓线)"""
  250. self.vertices = np.array(vertices, dtype=np.float32)
  251. self.colors = np.array(colors, dtype=np.float32)
  252. if triangles is not None:
  253. self.triangles = np.array(triangles, dtype=np.int32)
  254. else:
  255. self.triangles = np.array([])
  256. if normals is not None:
  257. self.normals = np.array(normals, dtype=np.float32)
  258. else:
  259. self.normals = np.array([])
  260. if silhouette_edges is not None:
  261. self.edges = np.array(silhouette_edges, dtype=np.int32) # ← 存为 self.edges
  262. else:
  263. self.edges = np.array([])
  264. # ✅ 修正:打印 self.edges,不是 self.silhouette_edges
  265. print(f"✅ 设置数据: {len(self.vertices)} 个顶点, {len(self.triangles)} 个三角面")
  266. print(f" 轮廓线数量: {len(self.edges)} 条") # ✅ 正确字段名
  267. self.update()
  268. def mousePressEvent(self, event):
  269. self.last_mouse_pos = event.pos() #二维坐标,仅用来计算旋转/移动的量
  270. if event.button() == Qt.LeftButton:
  271. if self.set_color == True:
  272. self._do_color_pick(event.pos())
  273. if self.picking == True:
  274. self._do_picking(event.pos())
  275. super().mousePressEvent(event) #调用父类中默认的方法,获得预设的一些功能
  276. def _do_color_pick(self, pos):
  277. """
  278. 点击屏幕某点,找到对应的三角形,
  279. 然后通过“法线连通性”扩展出整个平面区域,
  280. 将该区域所有三角形的顶点颜色设为白色(或加深)
  281. """
  282. # 1. 射线拾取:获取被点击的三角形索引
  283. picked_tri_idx = self._ray_cast_triangle_index(pos)
  284. if picked_tri_idx is None:
  285. return # 没点中任何面
  286. print(f"选中三角形: {picked_tri_idx}")
  287. # 2. 获取该三角形的法线(作为基准)
  288. base_normal = self.face_normals[picked_tri_idx]
  289. tolerance = 0.05 # 法线夹角余弦容忍度(越小越严格)
  290. # 3. 使用广度优先搜索(BFS)扩展所有“共面”三角形
  291. from collections import deque
  292. queue = deque([picked_tri_idx])
  293. visited = set()
  294. visited.add(picked_tri_idx)
  295. region_triangles = [] # 存储属于这个“面区域”的所有三角面索引
  296. while queue:
  297. tri_idx = queue.popleft()
  298. region_triangles.append(tri_idx)
  299. # 获取当前三角形的三个顶点
  300. v0, v1, v2 = self.triangles[tri_idx]
  301. all_vertices = [v0, v1, v2]
  302. # 遍历所有邻接三角形(共享边的三角形)
  303. for i in range(3):
  304. v_a = all_vertices[i]
  305. v_b = all_vertices[(i + 1) % 3]
  306. edge = tuple(sorted([v_a, v_b])) # 边(无序)
  307. # 查找共享这条边的其他三角形(需预建边到面的映射,或遍历)
  308. # 这里简化:遍历所有三角形找邻接
  309. for neighbor_idx in range(len(self.triangles)):
  310. if neighbor_idx in visited:
  311. continue
  312. tri = self.triangles[neighbor_idx]
  313. tri_edges = [
  314. tuple(sorted([tri[0], tri[1]])),
  315. tuple(sorted([tri[1], tri[2]])),
  316. tuple(sorted([tri[2], tri[0]])),
  317. ]
  318. if edge in tri_edges:
  319. # 是邻接三角形,检查法线是否接近
  320. neighbor_normal = self.face_normals[neighbor_idx]
  321. dot = np.dot(base_normal, neighbor_normal)
  322. if abs(dot) > (1 - tolerance): # 法线夹角小
  323. visited.add(neighbor_idx)
  324. queue.append(neighbor_idx)
  325. # ===================================
  326. # ✅ 核心修改:不再修改顶点颜色
  327. # 而是将共面区域的三角形索引保存到 self.highlighted_face_indices
  328. # 由 paintGL 在渲染时决定是否高亮
  329. # ===================================
  330. self.highlighted_face_indices = region_triangles
  331. # 可选:打印调试信息
  332. print(f"✅ 共面区域包含 {len(self.highlighted_face_indices)} 个三角形")
  333. # 触发重绘
  334. self.update() # 调用 paintGL 重新渲染
  335. def _is_adjacent(self, tri_idx1, tri_idx2):
  336. """判断两个三角形是否共享一条边(即邻接)"""
  337. v1 = set(self.triangles[tri_idx1])
  338. v2 = set(self.triangles[tri_idx2])
  339. return len(v1 & v2) == 2 # 共享两个顶点 → 共享一条边
  340. def _ray_cast_triangle_index(self, pos):
  341. """
  342. 从屏幕点击位置发射一条射线,检测与哪个三角形相交(最近的)
  343. 返回相交的三角形索引,无则返回 None
  344. """
  345. width, height = self.width(), self.height()
  346. x, y = pos.x(), pos.y()
  347. # 1. 获取当前模型视图和投影矩阵
  348. modelview = np.array(self.get_current_matrix()[0], dtype=np.float64)
  349. projection = np.array(self.get_current_matrix()[1], dtype=np.float64)
  350. viewport = (0, 0, width, height)
  351. # 2. 将屏幕坐标转换为世界空间中的两个点(近平面和远平面)
  352. try:
  353. # 注意:OpenGL 的 Y 轴向下,所以要翻转 y
  354. world_near = gluUnProject(x, height - y, 0.0, modelview, projection, viewport)
  355. world_far = gluUnProject(x, height - y, 1.0, modelview, projection, viewport)
  356. except Exception as e:
  357. print(f"gluUnProject failed: {e}")
  358. return None
  359. # 3. 构造射线方向
  360. ray_origin = np.array(world_near)
  361. ray_direction = np.array(world_far) - np.array(world_near)
  362. ray_direction = ray_direction / (np.linalg.norm(ray_direction) + 1e-8)
  363. # 4. 遍历所有三角形,做射线相交检测
  364. best_t = float('inf')
  365. picked_idx = None
  366. for idx, triangle in enumerate(self.triangles):
  367. v0_idx, v1_idx, v2_idx = triangle
  368. v0 = np.array(self.vertices[v0_idx])
  369. v1 = np.array(self.vertices[v1_idx])
  370. v2 = np.array(self.vertices[v2_idx])
  371. result = self._ray_triangle_intersect(
  372. ray_origin, ray_direction,
  373. v0, v1, v2
  374. )
  375. if result is not None:
  376. t, u, v = result
  377. if t < best_t and t > 0:
  378. best_t = t
  379. picked_idx = idx
  380. return picked_idx
  381. def _ray_triangle_intersect(self, ray_origin, ray_direction, v0, v1, v2, eps=1e-6):
  382. """
  383. Möller–Trumbore 射线-三角形相交算法
  384. 返回: (t, u, v) 或 None(无交点)
  385. t: 射线上交点距离
  386. u,v: 重心坐标
  387. """
  388. # 三角形边向量
  389. edge1 = v1 - v0
  390. edge2 = v2 - v0
  391. # P = ray_direction × edge2
  392. P = np.cross(ray_direction, edge2)
  393. det = np.dot(edge1, P)
  394. if abs(det) < eps:
  395. return None # 射线与三角形平行
  396. inv_det = 1.0 / det
  397. T = ray_origin - v0
  398. u = np.dot(T, P) * inv_det
  399. if u < 0.0 or u > 1.0:
  400. return None
  401. Q = np.cross(T, edge1)
  402. v = np.dot(ray_direction, Q) * inv_det
  403. if v < 0.0 or u + v > 1.0:
  404. return None
  405. t = np.dot(edge2, Q) * inv_det
  406. if t > eps:
  407. return t, u, v # 相交
  408. return None
  409. from OpenGL.GL import glGetDoublev, GL_MODELVIEW_MATRIX, GL_PROJECTION
  410. def get_current_matrix(self):
  411. """
  412. 安全获取当前 Modelview 和 Projection 矩阵
  413. 必须在 OpenGL 上下文中调用(比如在 paintGL 之后)
  414. """
  415. try:
  416. modelview = glGetDoublev(GL_MODELVIEW_MATRIX)
  417. projection = glGetDoublev(GL_PROJECTION_MATRIX) # 注意:是 GL_PROJECTION_MATRIX,不是 GL_PROJECTION
  418. return modelview, projection
  419. except Exception as e:
  420. print(f"获取矩阵失败: {e}")
  421. return None, None
  422. def _do_picking(self, pos):
  423. x, y = pos.x(), pos.y()
  424. self.makeCurrent()
  425. self._render_for_picking() #再渲染一个当前的模型到gpu
  426. #提取pos坐标像素点在窗口中的实际颜色piexel
  427. pixel = glReadPixels(x, self.height() - y - 1, 1, 1, GL_RGB, GL_FLOAT)
  428. #将rgb分量的值提取出来
  429. r, g, b = pixel[0][0]
  430. print(f"Read color: ({r:.5f}, {g:.5f}, {b:.5f})")
  431. # ✅ 关键修复:转成 float 再 round,避免虽然值一样,但 Python 字典认为 np.float32(0.01176) ≠ float(0.01176)!
  432. key = (round(float(r), 5), round(float(g), 5), round(float(b), 5))
  433. if key in self.picking_color_map:
  434. index = self.picking_color_map[key]
  435. picked_point = self.vertices[index]
  436. self.selected_point = picked_point
  437. print(f"🎯 拾取到点: {picked_point}, 索引: {index}")
  438. self.pointPicked.emit(*picked_point)
  439. else:
  440. print(f"❌ 未找到对应点,key={key} 不在 map 中")
  441. self.update()
  442. def mouseMoveEvent(self, event):
  443. if self.last_mouse_pos is None:
  444. return
  445. dx = event.x() - self.last_mouse_pos.x()
  446. dy = event.y() - self.last_mouse_pos.y()
  447. is_ctrl = event.modifiers() & Qt.ControlModifier
  448. if event.buttons() & Qt.LeftButton:
  449. if is_ctrl:
  450. # Ctrl + 左键:旋转世界(视角)
  451. self.view_rotation[0] += dy * 0.5 # 俯仰
  452. self.view_rotation[1] += dx * 0.5 # 偏航
  453. self.view_rotation[0] = max(-89.0, min(89.0, self.view_rotation[0]))
  454. else:
  455. # 左键:旋转模型
  456. self.model_rotation[0] += dy * 0.5
  457. self.model_rotation[1] += dx * 0.5
  458. elif event.buttons() & Qt.RightButton:
  459. if is_ctrl:
  460. # Ctrl + 右键:平移世界(视点平移)
  461. self.view_pan[0] += dx * 0.01
  462. self.view_pan[1] -= dy * 0.01
  463. else:
  464. # 右键:平移模型
  465. self.model_pan[0] += dx * 0.01
  466. self.model_pan[1] -= dy * 0.01
  467. self.last_mouse_pos = event.pos()
  468. self.update()
  469. def wheelEvent(self, event):
  470. delta = event.angleDelta().y()
  471. #view_distance 相机到模型的距离,delta:鼠标滚动的值
  472. self.view_distance -= delta * 0.005
  473. #距离的范围限定
  474. self.view_distance = max(1.0, min(50.0, self.view_distance))
  475. self.update()
  476. def toggle_display_mode(self):
  477. """切换显示模式:点云 <-> 表面"""
  478. if self.display_mode == 'points':
  479. self.display_mode = 'surface'
  480. else:
  481. self.display_mode = 'points'
  482. self.update() # 切换模式后重新绘制
  483. def toggle_axes_display_mode(self):
  484. "切换坐标系显示模式"
  485. if self.axes_display_mode == False:
  486. self.axes_display_mode = True
  487. else:
  488. self.axes_display_mode = False
  489. self.update()
  490. def _generate_picking_colors(self):
  491. """为每个顶点生成唯一颜色(用于拾取)"""
  492. if len(self.vertices) == 0:
  493. return np.array([])
  494. colors = np.zeros((len(self.vertices), 3), dtype=np.float32)
  495. for i in range(len(self.vertices)):
  496. # 用整数编码成 RGB(最多支持 ~1677 万个点)
  497. color_id = i + 1 # 从 1 开始,避免 0,0,0(黑色)误判
  498. r = (color_id & 0xFF) / 255.0
  499. g = ((color_id >> 8) & 0xFF) / 255.0
  500. b = ((color_id >> 16) & 0xFF) / 255.0
  501. colors[i] = [r, g, b]
  502. self.picking_color_map[(r, g, b)] = i # 反向映射
  503. return colors
  504. def _render_for_picking(self):
  505. if len(self.vertices) == 0:
  506. return
  507. #设置断点::参数入栈函数,GL_ALL_ATTRIB_BITS:当前OPENGL的所有参数
  508. glPushAttrib(GL_ALL_ATTRIB_BITS)
  509. #关闭光照计算,避免编码颜色改变
  510. glDisable(GL_LIGHTING)
  511. #关闭纹理映射
  512. glDisable(GL_TEXTURE_2D)
  513. #设置平面着色:一个三角面中的颜色保持一致
  514. glShadeModel(GL_FLAT)
  515. #设置清屏颜色为黑色
  516. glClearColor(0, 0, 0, 0)
  517. #清理颜色缓存和深度缓存
  518. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  519. #开启深度测试
  520. glEnable(GL_DEPTH_TEST)
  521. # --- 复制 paintGL 的投影和视图变换 ---
  522. #设置矩阵模式:投影矩阵
  523. glMatrixMode(GL_PROJECTION)
  524. #重置投影矩阵为单位矩阵
  525. glLoadIdentity()
  526. #设置透视投影
  527. gluPerspective(45, self.width() / max(self.height(), 1), 0.1, 100.0)
  528. #切换矩阵模式:模型视图矩阵
  529. glMatrixMode(GL_MODELVIEW)
  530. #重置当前矩阵为单位矩阵
  531. glLoadIdentity()
  532. yaw = self.view_rotation[1]
  533. pitch = self.view_rotation[0]
  534. distance = self.view_distance
  535. cam_x = distance * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch))
  536. cam_y = distance * np.sin(np.radians(pitch))
  537. cam_z = distance * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch))
  538. gluLookAt(cam_x, cam_y, cam_z, 0, 0, 0, 0, 1, 0)
  539. glTranslatef(self.view_pan[0], self.view_pan[1], 0)
  540. glPushMatrix()
  541. glTranslatef(self.model_pan[0], self.model_pan[1], 0)
  542. glRotatef(self.model_rotation[0], 1, 0, 0)
  543. glRotatef(self.model_rotation[1], 0, 1, 0)
  544. glScalef(self.model_scale, self.model_scale, self.model_scale)
  545. # ✅ 用唯一颜色绘制顶点
  546. glPointSize(8.0)
  547. glBegin(GL_POINTS)
  548. for i, v in enumerate(self.vertices):
  549. idx = i + 1
  550. r = (idx) & 0xFF
  551. g = (idx >> 8) & 0xFF
  552. b = (idx >> 16) & 0xFF
  553. glColor3f(r / 255.0, g / 255.0, b / 255.0)
  554. glVertex3f(v[0], v[1], v[2])
  555. glEnd()
  556. glPopMatrix()
  557. glPopAttrib()
  558. def point_mode(self):
  559. """切换选点模式"""
  560. if self.picking == False:
  561. self.picking = True
  562. self.set_color = False
  563. print(f"已进入选点模式:{self.picking}")
  564. else:
  565. self.picking = False
  566. print(f"已退出选点模式:{self.picking}")
  567. # self.update() # 切换模式后重新绘制
  568. def color_mode(self):
  569. """切换选色模式"""
  570. if self.set_color == False:
  571. self.set_color = True
  572. self.picking = False #打开选色则关闭选点
  573. print(f"已进入选色模式:{self.set_color}")
  574. else:
  575. self.set_color = False
  576. print(f"已退出选色模式:{self.set_color}")
  577. #统一颜色显示方式
  578. def _get_picking_color(self, index):
  579. idx = index + 1
  580. r = (idx) & 0xFF
  581. g = (idx >> 8) & 0xFF
  582. b = (idx >> 16) & 0xFF
  583. return (r / 255.0, g / 255.0, b / 255.0)