glWidget_simple.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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. def initializeGL(self):
  33. """初始化 OpenGL 状态"""
  34. print("✅ 3D OpenGL 初始化")
  35. glEnable(GL_DEPTH_TEST)
  36. glEnable(GL_COLOR_MATERIAL)
  37. glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
  38. glEnable(GL_LIGHT0)
  39. glLightfv(GL_LIGHT0, GL_POSITION, (1.0, 1.0, 1.0, 0.0)) # 平行光
  40. glClearColor(0.1, 0.1, 0.1, 1.0)
  41. def resizeGL(self, width, height):
  42. """窗口大小改变时调用"""
  43. print(f"✅ 调整大小: {width}x{height}")
  44. if height == 0:
  45. height = 1
  46. glViewport(0, 0, width, height)
  47. self.update() # 重新绘制
  48. def paintGL(self):
  49. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  50. glEnable(GL_DEPTH_TEST)
  51. glMatrixMode(GL_PROJECTION)
  52. glLoadIdentity()
  53. gluPerspective(45, self.width() / max(self.height(), 1), 0.1, 100.0)
  54. glMatrixMode(GL_MODELVIEW)
  55. glLoadIdentity()
  56. # ✅ 使用 gluLookAt 构建轨道摄像机
  57. # 视角参数
  58. yaw = self.view_rotation[1] # 偏航角(左右)
  59. pitch = self.view_rotation[0] # 俯仰角(上下)
  60. distance = self.view_distance
  61. # 1. 计算摄像机位置(绕原点轨道)
  62. radius = distance
  63. cam_x = radius * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch))
  64. cam_y = radius * np.sin(np.radians(pitch))
  65. cam_z = radius * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch))
  66. # 2. 目标点 = 世界原点 (0,0,0)
  67. target_x, target_y, target_z = 0.0, 0.0, 0.0
  68. # 3. 上方向(防止翻滚)
  69. up_x, up_y, up_z = 0.0, 1.0, 0.0
  70. # ✅ 设置摄像机(绕原点轨道旋转)
  71. gluLookAt(
  72. cam_x, cam_y, cam_z, # 摄像机位置
  73. target_x, target_y, target_z, # 看向目标(世界原点)
  74. up_x, up_y, up_z # 上方向
  75. )
  76. # ✅ 应用视点平移(Ctrl+右键平移)
  77. glTranslatef(self.view_pan[0], self.view_pan[1], 0)
  78. # ✅ 绘制世界坐标系(在原点)
  79. if self.axes_world_display_mode:
  80. self.drawWorldAxes()
  81. # ✅ 模型变换
  82. glPushMatrix()
  83. glTranslatef(self.model_pan[0], self.model_pan[1], 0)
  84. glRotatef(self.model_rotation[0], 1, 0, 0)
  85. glRotatef(self.model_rotation[1], 0, 1, 0)
  86. glScalef(self.model_scale, self.model_scale, self.model_scale)
  87. if self.axes_display_mode:
  88. self.drawModelAxes()
  89. self._draw_model()
  90. glPopMatrix()
  91. def _draw_model(self):
  92. """私有方法:绘制模型"""
  93. if self.display_mode == 'points' and len(self.vertices) > 0:
  94. glPointSize(8.0)
  95. glBegin(GL_POINTS)
  96. for i, v in enumerate(self.vertices):
  97. if i < len(self.colors):
  98. glColor3f(*self.colors[i])
  99. glVertex3f(v[0], v[1], v[2])
  100. glEnd()
  101. elif self.display_mode == 'surface' and len(self.triangles) > 0:
  102. glEnable(GL_LIGHTING)
  103. glEnable(GL_LIGHT0)
  104. glBegin(GL_TRIANGLES)
  105. for tri in self.triangles:
  106. for idx in tri:
  107. if len(self.normals) > idx:
  108. n = self.normals[idx]
  109. if not np.any(np.isnan(n)) and not np.any(np.isinf(n)):
  110. glNormal3f(*n)
  111. if len(self.colors) > idx:
  112. glColor3f(*self.colors[idx])
  113. glVertex3f(*self.vertices[idx])
  114. glEnd()
  115. glDisable(GL_LIGHTING)
  116. def renderText(self, x, y, z, text):
  117. """
  118. 在指定的三维坐标位置渲染文本。
  119. :param x: X轴坐标
  120. :param y: Y轴坐标
  121. :param z: Z轴坐标
  122. :param text: 要渲染的文本内容
  123. """
  124. glRasterPos3f(x, y, z) # 设置文本位置
  125. for ch in text:
  126. glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, ord(ch)) # 渲染每个字符
  127. def drawWorldAxes(self):
  128. """绘制固定的世界坐标系(左下角)"""
  129. glPushMatrix()
  130. # 移动到左下角
  131. glTranslatef(-4.0, -4.0, -5.0)
  132. glLineWidth(2.0)
  133. glBegin(GL_LINES)
  134. # X (红)
  135. glColor3f(1, 0, 0)
  136. glVertex3f(0, 0, 0);
  137. glVertex3f(1000, 0, 0)
  138. # Y (绿)
  139. glColor3f(0, 1, 0)
  140. glVertex3f(0, 0, 0);
  141. glVertex3f(0, 1000, 0)
  142. # Z (蓝)
  143. glColor3f(0, 0, 1)
  144. glVertex3f(0, 0, 0);
  145. glVertex3f(0, 0, 1000)
  146. glEnd()
  147. # 绘制文本标签
  148. glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色
  149. self.renderText(1.5, 0, 0, 'X') # X轴标签
  150. glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色
  151. self.renderText(0, 1.5, 0, 'Y') # Y轴标签
  152. glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色
  153. self.renderText(0, 0, 1.5, 'Z') # Z轴标签
  154. glPopMatrix()
  155. def drawModelAxes(self):
  156. """绘制随模型移动的坐标系"""
  157. glPushMatrix()
  158. glLineWidth(2.5)
  159. glBegin(GL_LINES)
  160. # X
  161. glColor3f(1, 0, 0)
  162. glVertex3f(0, 0, 0);
  163. glVertex3f(2, 0, 0)
  164. # Y
  165. glColor3f(0, 1, 0)
  166. glVertex3f(0, 0, 0);
  167. glVertex3f(0, 2, 0)
  168. # Z
  169. glColor3f(0, 0, 1)
  170. glVertex3f(0, 0, 0);
  171. glVertex3f(0, 0, 2)
  172. glEnd()
  173. # 绘制文本标签
  174. glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色
  175. self.renderText(1.5, 0, 0, 'X') # X轴标签
  176. glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色
  177. self.renderText(0, 1.5, 0, 'Y') # Y轴标签
  178. glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色
  179. self.renderText(0, 0, 1.5, 'Z') # Z轴标签
  180. glPopMatrix()
  181. # if hasattr(self, 'model_scale'):
  182. # glScalef(self.model_scale, self.model_scale, self.model_scale)
  183. # def paintGLAxes(self):
  184. # """绘制坐标轴"""
  185. # glLineWidth(3.0)
  186. # glBegin(GL_LINES)
  187. # # X轴 - 红色
  188. # glColor3f(1.0, 0.0, 0.0)
  189. # glVertex3f(0.0, 0.0, 0.0)
  190. # glVertex3f(100.0, 0.0, 0.0)
  191. #
  192. # # Y轴 - 绿色
  193. # glColor3f(0.0, 1.0, 0.0)
  194. # glVertex3f(0.0, 0.0, 0.0)
  195. # glVertex3f(0.0, 100.0, 0.0)
  196. #
  197. # # Z轴 - 蓝色
  198. # glColor3f(0.0, 0.0, 1.0)
  199. # glVertex3f(0.0, 0.0, 0.0)
  200. # glVertex3f(0.0, 0.0, 100.0)
  201. # glEnd()
  202. #
  203. # # 绘制文本标签
  204. # glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色
  205. # self.renderText(1.5, 0, 0, 'X') # X轴标签
  206. #
  207. # glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色
  208. # self.renderText(0, 1.5, 0, 'Y') # Y轴标签
  209. #
  210. # glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色
  211. # self.renderText(0, 0, 1.5, 'Z') # Z轴标签
  212. def set_data(self, vertices, colors, triangles=None, normals=None):
  213. """设置 3D 数据(支持 mesh)"""
  214. self.vertices = np.array(vertices, dtype=np.float32)
  215. self.colors = np.array(colors, dtype=np.float32)
  216. if triangles is not None:
  217. self.triangles = np.array(triangles, dtype=np.int32)
  218. else:
  219. self.triangles = np.array([])
  220. if normals is not None:
  221. self.normals = np.array(normals, dtype=np.float32)
  222. else:
  223. self.normals = np.array([])
  224. print(f"✅ 设置数据: {len(self.vertices)} 个顶点, {len(self.triangles)} 个三角面")
  225. self.update()
  226. def mousePressEvent(self, event):
  227. self.last_mouse_pos = event.pos()
  228. def mouseMoveEvent(self, event):
  229. if self.last_mouse_pos is None:
  230. return
  231. dx = event.x() - self.last_mouse_pos.x()
  232. dy = event.y() - self.last_mouse_pos.y()
  233. is_ctrl = event.modifiers() & Qt.ControlModifier
  234. if event.buttons() & Qt.LeftButton:
  235. if is_ctrl:
  236. # Ctrl + 左键:旋转世界(视角)
  237. self.view_rotation[0] += dy * 0.5 # 俯仰
  238. self.view_rotation[1] += dx * 0.5 # 偏航
  239. self.view_rotation[0] = max(-89.0, min(89.0, self.view_rotation[0]))
  240. else:
  241. # 左键:旋转模型
  242. self.model_rotation[0] += dy * 0.5
  243. self.model_rotation[1] += dx * 0.5
  244. elif event.buttons() & Qt.RightButton:
  245. if is_ctrl:
  246. # Ctrl + 右键:平移世界(视点平移)
  247. self.view_pan[0] += dx * 0.01
  248. self.view_pan[1] -= dy * 0.01
  249. else:
  250. # 右键:平移模型
  251. self.model_pan[0] += dx * 0.01
  252. self.model_pan[1] -= dy * 0.01
  253. self.last_mouse_pos = event.pos()
  254. self.update()
  255. def wheelEvent(self, event):
  256. delta = event.angleDelta().y()
  257. self.view_distance -= delta * 0.05
  258. self.view_distance = max(1.0, min(50.0, self.view_distance))
  259. self.update()
  260. def toggle_display_mode(self):
  261. """切换显示模式:点云 <-> 表面"""
  262. if self.display_mode == 'points':
  263. self.display_mode = 'surface'
  264. else:
  265. self.display_mode = 'points'
  266. self.update() # 切换模式后重新绘制
  267. def toggle_axes_display_mode(self):
  268. "切换坐标系显示模式"
  269. if self.axes_display_mode == False:
  270. self.axes_display_mode = True
  271. else:
  272. self.axes_display_mode = False
  273. self.update()