glWidget_simple.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import sys
  2. import numpy as np
  3. from PyQt5.QtWidgets import QApplication, QOpenGLWidget, QVBoxLayout, QWidget
  4. from PyQt5.QtCore import Qt, pyqtSignal
  5. from OpenGL.GL import *
  6. from OpenGL.GLU import *
  7. from OpenGL.GLUT import glutBitmapCharacter, glutStrokeCharacter, GLUT_BITMAP_HELVETICA_18, GLUT_BITMAP_TIMES_ROMAN_24 # GLUT_BITMAP_HELVETICA_18是运行时调用,标红正常
  8. class Simple3DWidget(QOpenGLWidget):
  9. pointPicked = pyqtSignal(float, float, float)
  10. def __init__(self, parent=None):
  11. super().__init__(parent)
  12. self.vertices = np.array([]) # 顶点数据
  13. self.colors = np.array([]) # 颜色数据
  14. self.triangles = np.array([], dtype=np.int32) # 三角面数据 ← 新增
  15. self.normals = np.array([]) # 法线数据 ← 新增
  16. self.rotation = [0.0, 0.0] # 旋转角度 [俯仰, 偏航]
  17. self.zoom = -5.0 # 视距(负值表示拉远)
  18. self.pan = [0.0, 0.0] # 平移偏移 [x, y]
  19. self.last_mouse_pos = None # 鼠标位置
  20. self.setMouseTracking(True) # 启用鼠标跟踪
  21. self.display_mode = 'points' # 默认显示模式: 点云
  22. self.axes_display_mode = False
  23. self.axes_world_display_mode = True
  24. # 🔧 模型变换
  25. self.model_rotation = [0.0, 0.0] # 模型的 [俯仰, 偏航]
  26. self.model_pan = [0.0, 0.0] # 模型平移 (x, y)
  27. self.model_scale = 1.0 # 模型缩放
  28. # 🎯 视角(世界)变换
  29. self.view_rotation = [0.0, 0.0] # 模型的 [俯仰, 偏航]
  30. self.view_pan = [0.0, 0.0] # 视点平移 (X, Y),用于 Ctrl+右键
  31. self.view_distance = 8.0 # 视点到目标的距离
  32. #选点模式
  33. self.selected_point = None # 存储选中的点坐标
  34. self.picking = False # 是否处于拾取模式
  35. self.picking_color_map = {} # 顶点索引 → 唯一颜色(用于反查)
  36. def initializeGL(self):
  37. """初始化 OpenGL 状态"""
  38. print("✅ 3D OpenGL 初始化")
  39. glEnable(GL_DEPTH_TEST)
  40. glEnable(GL_COLOR_MATERIAL)
  41. glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
  42. glEnable(GL_LIGHT0)
  43. glLightfv(GL_LIGHT0, GL_POSITION, (1.0, 1.0, 1.0, 0.0)) # 平行光
  44. glClearColor(0.1, 0.1, 0.1, 1.0)
  45. def resizeGL(self, width, height):
  46. """窗口大小改变时调用"""
  47. print(f"✅ 调整大小: {width}x{height}")
  48. if height == 0:
  49. height = 1
  50. glViewport(0, 0, width, height)
  51. self.update() # 重新绘制
  52. def paintGL(self):
  53. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  54. glEnable(GL_DEPTH_TEST)
  55. glMatrixMode(GL_PROJECTION)
  56. glLoadIdentity()
  57. gluPerspective(45, self.width() / max(self.height(), 1), 0.1, 100.0)
  58. glMatrixMode(GL_MODELVIEW)
  59. glLoadIdentity()
  60. # ✅ 使用 gluLookAt 构建轨道摄像机
  61. # 视角参数
  62. yaw = self.view_rotation[1] # 偏航角(左右)
  63. pitch = self.view_rotation[0] # 俯仰角(上下)
  64. distance = self.view_distance
  65. # 1. 计算摄像机位置(绕原点轨道)
  66. radius = distance
  67. cam_x = radius * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch))
  68. cam_y = radius * np.sin(np.radians(pitch))
  69. cam_z = radius * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch))
  70. # 2. 目标点 = 世界原点 (0,0,0)
  71. target_x, target_y, target_z = 0.0, 0.0, 0.0
  72. # 3. 上方向(防止翻滚)
  73. up_x, up_y, up_z = 0.0, 1.0, 0.0
  74. # ✅ 设置摄像机(绕原点轨道旋转)
  75. gluLookAt(
  76. cam_x, cam_y, cam_z, # 摄像机位置
  77. target_x, target_y, target_z, # 看向目标(世界原点)
  78. up_x, up_y, up_z # 上方向
  79. )
  80. # ✅ 应用视点平移(Ctrl+右键平移)
  81. glTranslatef(self.view_pan[0], self.view_pan[1], 0)
  82. # ✅ 绘制世界坐标系(在原点)
  83. if self.axes_world_display_mode:
  84. self.drawWorldAxes()
  85. # ✅ 模型变换
  86. glPushMatrix()
  87. glTranslatef(self.model_pan[0], self.model_pan[1], 0)
  88. glRotatef(self.model_rotation[0], 1, 0, 0)
  89. glRotatef(self.model_rotation[1], 0, 1, 0)
  90. glScalef(self.model_scale, self.model_scale, self.model_scale)
  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. glPointSize(8.0)
  101. glBegin(GL_POINTS)
  102. try:
  103. for i, v in enumerate(self.vertices):
  104. # 判断是否是选中点
  105. if self.selected_point is not None and np.allclose(v, self.selected_point, atol=1e-6):
  106. glColor3f(1.0, 0.0, 0.0) # 红色高亮
  107. else:
  108. glColor3f(*self.colors[i]) # 原始颜色
  109. glVertex3f(v[0], v[1], v[2])
  110. finally:
  111. glEnd() # ✅ 保证执行
  112. elif self.display_mode == 'surface' and len(self.triangles) > 0:
  113. # ✅ 开启光照
  114. glEnable(GL_LIGHTING)
  115. try:
  116. # ✅ 预计算选中点的索引(避免在 glBegin 内部调用 np.allclose)
  117. highlight_indices = set()
  118. if self.selected_point is not None:
  119. for idx, v in enumerate(self.vertices):
  120. if np.allclose(v, self.selected_point, atol=1e-6):
  121. highlight_indices.add(idx)
  122. glBegin(GL_TRIANGLES)
  123. try:
  124. for tri in self.triangles:
  125. for idx in tri:
  126. # 高亮三角形中包含选中点的顶点
  127. if idx in highlight_indices:
  128. glColor3f(1.0, 0.0, 0.0)
  129. else:
  130. glColor3f(*self.colors[idx])
  131. # 设置法线(如果存在且有效)
  132. if len(self.normals) > idx:
  133. n = self.normals[idx]
  134. if not np.any(np.isnan(n)) and not np.any(np.isinf(n)):
  135. glNormal3f(*n)
  136. glVertex3f(*self.vertices[idx])
  137. finally:
  138. glEnd() # ✅ 保证结束绘制
  139. finally:
  140. glDisable(GL_LIGHTING) # ✅ 保证关闭光照
  141. def renderText(self, x, y, z, text):
  142. """
  143. 在指定的三维坐标位置渲染文本。
  144. :param x: X轴坐标
  145. :param y: Y轴坐标
  146. :param z: Z轴坐标
  147. :param text: 要渲染的文本内容
  148. """
  149. glRasterPos3f(x, y, z) # 设置文本位置
  150. for ch in text:
  151. glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, ord(ch)) # 渲染每个字符
  152. def drawWorldAxes(self):
  153. """绘制固定的世界坐标系(左下角)"""
  154. glPushMatrix()
  155. try:
  156. # 移动到左下角
  157. glTranslatef(-4.0, -4.0, -5.0)
  158. glLineWidth(2.0)
  159. glBegin(GL_LINES)
  160. # X (红)
  161. glColor3f(1, 0, 0)
  162. glVertex3f(0, 0, 0)
  163. glVertex3f(1000, 0, 0)
  164. # Y (绿)
  165. glColor3f(0, 1, 0)
  166. glVertex3f(0, 0, 0)
  167. glVertex3f(0, 1000, 0)
  168. # Z (蓝)
  169. glColor3f(0, 0, 1)
  170. glVertex3f(0, 0, 0)
  171. glVertex3f(0, 0, 1000)
  172. glEnd() # 结束绘制
  173. # ✅ 确保 glEnd() 后再绘制文本,避免 OpenGL 状态混乱
  174. # 绘制文本标签
  175. glColor3f(1.0, 0.0, 0.0)
  176. self.renderText(1.5, 0, 0, 'X')
  177. glColor3f(0.0, 1.0, 0.0)
  178. self.renderText(0, 1.5, 0, 'Y')
  179. glColor3f(0.0, 0.0, 1.0)
  180. self.renderText(0, 0, 1.5, 'Z')
  181. finally:
  182. # ✅ 无论是否出错,都确保弹出矩阵栈
  183. glPopMatrix()
  184. def drawModelAxes(self):
  185. """绘制随模型移动的坐标系"""
  186. glPushMatrix()
  187. glLineWidth(2.5)
  188. glBegin(GL_LINES)
  189. # X
  190. glColor3f(1, 0, 0)
  191. glVertex3f(0, 0, 0);
  192. glVertex3f(2, 0, 0)
  193. # Y
  194. glColor3f(0, 1, 0)
  195. glVertex3f(0, 0, 0);
  196. glVertex3f(0, 2, 0)
  197. # Z
  198. glColor3f(0, 0, 1)
  199. glVertex3f(0, 0, 0);
  200. glVertex3f(0, 0, 2)
  201. glEnd()
  202. # 绘制文本标签
  203. glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色
  204. self.renderText(1.5, 0, 0, 'X') # X轴标签
  205. glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色
  206. self.renderText(0, 1.5, 0, 'Y') # Y轴标签
  207. glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色
  208. self.renderText(0, 0, 1.5, 'Z') # Z轴标签
  209. glPopMatrix()
  210. def set_data(self, vertices, colors, triangles=None, normals=None):
  211. """设置 3D 数据(支持 mesh)"""
  212. self.vertices = np.array(vertices, dtype=np.float32)
  213. self.colors = np.array(colors, dtype=np.float32)
  214. if triangles is not None:
  215. self.triangles = np.array(triangles, dtype=np.int32)
  216. else:
  217. self.triangles = np.array([])
  218. if normals is not None:
  219. self.normals = np.array(normals, dtype=np.float32)
  220. else:
  221. self.normals = np.array([])
  222. print(f"✅ 设置数据: {len(self.vertices)} 个顶点, {len(self.triangles)} 个三角面")
  223. self.update()
  224. def mousePressEvent(self, event):
  225. self.last_mouse_pos = event.pos() #二维坐标,仅用来计算旋转/移动的量
  226. if event.button() == Qt.LeftButton:
  227. if self.picking == True:
  228. self._do_picking(event.pos())
  229. super().mousePressEvent(event) #调用父类中默认的方法,获得预设的一些功能
  230. def _do_picking(self, pos):
  231. x, y = pos.x(), pos.y()
  232. self.makeCurrent()
  233. self._render_for_picking()
  234. pixel = glReadPixels(x, self.height() - y - 1, 1, 1, GL_RGB, GL_FLOAT)
  235. r, g, b = pixel[0][0]
  236. print(f"Read color: ({r:.5f}, {g:.5f}, {b:.5f})")
  237. # ✅ 关键修复:转成 float 再 round,避免虽然值一样,但 Python 字典认为 np.float32(0.01176) ≠ float(0.01176)!
  238. key = (round(float(r), 5), round(float(g), 5), round(float(b), 5))
  239. if key in self.picking_color_map:
  240. index = self.picking_color_map[key]
  241. picked_point = self.vertices[index]
  242. self.selected_point = picked_point
  243. print(f"🎯 拾取到点: {picked_point}, 索引: {index}")
  244. self.pointPicked.emit(*picked_point)
  245. else:
  246. print(f"❌ 未找到对应点,key={key} 不在 map 中")
  247. self.update()
  248. def mouseMoveEvent(self, event):
  249. if self.last_mouse_pos is None:
  250. return
  251. dx = event.x() - self.last_mouse_pos.x()
  252. dy = event.y() - self.last_mouse_pos.y()
  253. is_ctrl = event.modifiers() & Qt.ControlModifier
  254. if event.buttons() & Qt.LeftButton:
  255. if is_ctrl:
  256. # Ctrl + 左键:旋转世界(视角)
  257. self.view_rotation[0] += dy * 0.5 # 俯仰
  258. self.view_rotation[1] += dx * 0.5 # 偏航
  259. self.view_rotation[0] = max(-89.0, min(89.0, self.view_rotation[0]))
  260. else:
  261. # 左键:旋转模型
  262. self.model_rotation[0] += dy * 0.5
  263. self.model_rotation[1] += dx * 0.5
  264. elif event.buttons() & Qt.RightButton:
  265. if is_ctrl:
  266. # Ctrl + 右键:平移世界(视点平移)
  267. self.view_pan[0] += dx * 0.01
  268. self.view_pan[1] -= dy * 0.01
  269. else:
  270. # 右键:平移模型
  271. self.model_pan[0] += dx * 0.01
  272. self.model_pan[1] -= dy * 0.01
  273. self.last_mouse_pos = event.pos()
  274. self.update()
  275. def wheelEvent(self, event):
  276. delta = event.angleDelta().y()
  277. #view_distance 相机到模型的距离,delta:鼠标滚动的值
  278. self.view_distance -= delta * 0.005
  279. #距离的范围限定
  280. self.view_distance = max(1.0, min(50.0, self.view_distance))
  281. self.update()
  282. def toggle_display_mode(self):
  283. """切换显示模式:点云 <-> 表面"""
  284. if self.display_mode == 'points':
  285. self.display_mode = 'surface'
  286. else:
  287. self.display_mode = 'points'
  288. self.update() # 切换模式后重新绘制
  289. def toggle_axes_display_mode(self):
  290. "切换坐标系显示模式"
  291. if self.axes_display_mode == False:
  292. self.axes_display_mode = True
  293. else:
  294. self.axes_display_mode = False
  295. self.update()
  296. def _generate_picking_colors(self):
  297. """为每个顶点生成唯一颜色(用于拾取)"""
  298. if len(self.vertices) == 0:
  299. return np.array([])
  300. colors = np.zeros((len(self.vertices), 3), dtype=np.float32)
  301. for i in range(len(self.vertices)):
  302. # 用整数编码成 RGB(最多支持 ~1677 万个点)
  303. color_id = i + 1 # 从 1 开始,避免 0,0,0(黑色)误判
  304. r = (color_id & 0xFF) / 255.0
  305. g = ((color_id >> 8) & 0xFF) / 255.0
  306. b = ((color_id >> 16) & 0xFF) / 255.0
  307. colors[i] = [r, g, b]
  308. self.picking_color_map[(r, g, b)] = i # 反向映射
  309. return colors
  310. def _render_for_picking(self):
  311. if len(self.vertices) == 0:
  312. return
  313. glPushAttrib(GL_ALL_ATTRIB_BITS)
  314. glDisable(GL_LIGHTING)
  315. glDisable(GL_TEXTURE_2D)
  316. glShadeModel(GL_FLAT)
  317. glClearColor(0, 0, 0, 0)
  318. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  319. glEnable(GL_DEPTH_TEST)
  320. # --- 复制 paintGL 的投影和视图变换 ---
  321. glMatrixMode(GL_PROJECTION)
  322. glLoadIdentity()
  323. gluPerspective(45, self.width() / max(self.height(), 1), 0.1, 100.0)
  324. glMatrixMode(GL_MODELVIEW)
  325. glLoadIdentity()
  326. yaw = self.view_rotation[1]
  327. pitch = self.view_rotation[0]
  328. distance = self.view_distance
  329. cam_x = distance * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch))
  330. cam_y = distance * np.sin(np.radians(pitch))
  331. cam_z = distance * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch))
  332. gluLookAt(cam_x, cam_y, cam_z, 0, 0, 0, 0, 1, 0)
  333. glTranslatef(self.view_pan[0], self.view_pan[1], 0)
  334. glPushMatrix()
  335. glTranslatef(self.model_pan[0], self.model_pan[1], 0)
  336. glRotatef(self.model_rotation[0], 1, 0, 0)
  337. glRotatef(self.model_rotation[1], 0, 1, 0)
  338. glScalef(self.model_scale, self.model_scale, self.model_scale)
  339. # ✅ 用唯一颜色绘制顶点
  340. glPointSize(8.0)
  341. glBegin(GL_POINTS)
  342. for i, v in enumerate(self.vertices):
  343. idx = i + 1
  344. r = (idx) & 0xFF
  345. g = (idx >> 8) & 0xFF
  346. b = (idx >> 16) & 0xFF
  347. glColor3f(r / 255.0, g / 255.0, b / 255.0)
  348. glVertex3f(v[0], v[1], v[2])
  349. glEnd()
  350. glPopMatrix()
  351. glPopAttrib()
  352. def point_mode(self):
  353. """切换选点模式"""
  354. if self.picking == False:
  355. self.picking = True
  356. print(f"已进入选点模式:{self.picking}")
  357. else:
  358. self.picking = False
  359. print(f"已退出选点模式:{self.picking}")
  360. # self.update() # 切换模式后重新绘制
  361. #统一颜色显示方式
  362. def _get_picking_color(self, index):
  363. idx = index + 1
  364. r = (idx) & 0xFF
  365. g = (idx >> 8) & 0xFF
  366. b = (idx >> 16) & 0xFF
  367. return (r / 255.0, g / 255.0, b / 255.0)