import sys import numpy as np from PyQt5.QtWidgets import QApplication, QOpenGLWidget, QVBoxLayout, QWidget from PyQt5.QtCore import Qt, pyqtSignal from OpenGL.GL import * from OpenGL.GLU import * from OpenGL.GLUT import glutBitmapCharacter, glutStrokeCharacter, GLUT_BITMAP_HELVETICA_18, GLUT_BITMAP_TIMES_ROMAN_24 # GLUT_BITMAP_HELVETICA_18是运行时调用,标红正常 class Simple3DWidget(QOpenGLWidget): pointPicked = pyqtSignal(float, float, float) def __init__(self, parent=None): super().__init__(parent) self.vertices = np.array([]) # 顶点数据 self.colors = np.array([]) # 颜色数据 self.triangles = np.array([], dtype=np.int32) # 三角面数据 ← 新增 self.normals = np.array([]) # 法线数据 ← 新增 self.rotation = [0.0, 0.0] # 旋转角度 [俯仰, 偏航] self.zoom = -5.0 # 视距(负值表示拉远) self.pan = [0.0, 0.0] # 平移偏移 [x, y] self.last_mouse_pos = None # 鼠标位置 self.setMouseTracking(True) # 启用鼠标跟踪 self.display_mode = 'points' # 默认显示模式: 点云 self.axes_display_mode = False self.axes_world_display_mode = True # 🔧 模型变换 self.model_rotation = [0.0, 0.0] # 模型的 [俯仰, 偏航] self.model_pan = [0.0, 0.0] # 模型平移 (x, y) self.model_scale = 1.0 # 模型缩放 # 🎯 视角(世界)变换 self.view_rotation = [0.0, 0.0] # 模型的 [俯仰, 偏航] self.view_pan = [0.0, 0.0] # 视点平移 (X, Y),用于 Ctrl+右键 self.view_distance = 8.0 # 视点到目标的距离 def initializeGL(self): """初始化 OpenGL 状态""" print("✅ 3D OpenGL 初始化") glEnable(GL_DEPTH_TEST) glEnable(GL_COLOR_MATERIAL) glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE) glEnable(GL_LIGHT0) glLightfv(GL_LIGHT0, GL_POSITION, (1.0, 1.0, 1.0, 0.0)) # 平行光 glClearColor(0.1, 0.1, 0.1, 1.0) def resizeGL(self, width, height): """窗口大小改变时调用""" print(f"✅ 调整大小: {width}x{height}") if height == 0: height = 1 glViewport(0, 0, width, height) self.update() # 重新绘制 def paintGL(self): glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) glEnable(GL_DEPTH_TEST) glMatrixMode(GL_PROJECTION) glLoadIdentity() gluPerspective(45, self.width() / max(self.height(), 1), 0.1, 100.0) glMatrixMode(GL_MODELVIEW) glLoadIdentity() # ✅ 使用 gluLookAt 构建轨道摄像机 # 视角参数 yaw = self.view_rotation[1] # 偏航角(左右) pitch = self.view_rotation[0] # 俯仰角(上下) distance = self.view_distance # 1. 计算摄像机位置(绕原点轨道) radius = distance cam_x = radius * np.cos(np.radians(yaw)) * np.cos(np.radians(pitch)) cam_y = radius * np.sin(np.radians(pitch)) cam_z = radius * np.sin(np.radians(yaw)) * np.cos(np.radians(pitch)) # 2. 目标点 = 世界原点 (0,0,0) target_x, target_y, target_z = 0.0, 0.0, 0.0 # 3. 上方向(防止翻滚) up_x, up_y, up_z = 0.0, 1.0, 0.0 # ✅ 设置摄像机(绕原点轨道旋转) gluLookAt( cam_x, cam_y, cam_z, # 摄像机位置 target_x, target_y, target_z, # 看向目标(世界原点) up_x, up_y, up_z # 上方向 ) # ✅ 应用视点平移(Ctrl+右键平移) glTranslatef(self.view_pan[0], self.view_pan[1], 0) # ✅ 绘制世界坐标系(在原点) if self.axes_world_display_mode: self.drawWorldAxes() # ✅ 模型变换 glPushMatrix() glTranslatef(self.model_pan[0], self.model_pan[1], 0) glRotatef(self.model_rotation[0], 1, 0, 0) glRotatef(self.model_rotation[1], 0, 1, 0) glScalef(self.model_scale, self.model_scale, self.model_scale) if self.axes_display_mode: self.drawModelAxes() self._draw_model() glPopMatrix() def _draw_model(self): """私有方法:绘制模型""" if self.display_mode == 'points' and len(self.vertices) > 0: glPointSize(8.0) glBegin(GL_POINTS) for i, v in enumerate(self.vertices): if i < len(self.colors): glColor3f(*self.colors[i]) glVertex3f(v[0], v[1], v[2]) glEnd() elif self.display_mode == 'surface' and len(self.triangles) > 0: glEnable(GL_LIGHTING) glEnable(GL_LIGHT0) glBegin(GL_TRIANGLES) for tri in self.triangles: for idx in tri: if len(self.normals) > idx: n = self.normals[idx] if not np.any(np.isnan(n)) and not np.any(np.isinf(n)): glNormal3f(*n) if len(self.colors) > idx: glColor3f(*self.colors[idx]) glVertex3f(*self.vertices[idx]) glEnd() glDisable(GL_LIGHTING) def renderText(self, x, y, z, text): """ 在指定的三维坐标位置渲染文本。 :param x: X轴坐标 :param y: Y轴坐标 :param z: Z轴坐标 :param text: 要渲染的文本内容 """ glRasterPos3f(x, y, z) # 设置文本位置 for ch in text: glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, ord(ch)) # 渲染每个字符 def drawWorldAxes(self): """绘制固定的世界坐标系(左下角)""" glPushMatrix() # 移动到左下角 glTranslatef(-4.0, -4.0, -5.0) glLineWidth(2.0) glBegin(GL_LINES) # X (红) glColor3f(1, 0, 0) glVertex3f(0, 0, 0); glVertex3f(1000, 0, 0) # Y (绿) glColor3f(0, 1, 0) glVertex3f(0, 0, 0); glVertex3f(0, 1000, 0) # Z (蓝) glColor3f(0, 0, 1) glVertex3f(0, 0, 0); glVertex3f(0, 0, 1000) glEnd() # 绘制文本标签 glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色 self.renderText(1.5, 0, 0, 'X') # X轴标签 glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色 self.renderText(0, 1.5, 0, 'Y') # Y轴标签 glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色 self.renderText(0, 0, 1.5, 'Z') # Z轴标签 glPopMatrix() def drawModelAxes(self): """绘制随模型移动的坐标系""" glPushMatrix() glLineWidth(2.5) glBegin(GL_LINES) # X glColor3f(1, 0, 0) glVertex3f(0, 0, 0); glVertex3f(2, 0, 0) # Y glColor3f(0, 1, 0) glVertex3f(0, 0, 0); glVertex3f(0, 2, 0) # Z glColor3f(0, 0, 1) glVertex3f(0, 0, 0); glVertex3f(0, 0, 2) glEnd() # 绘制文本标签 glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色 self.renderText(1.5, 0, 0, 'X') # X轴标签 glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色 self.renderText(0, 1.5, 0, 'Y') # Y轴标签 glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色 self.renderText(0, 0, 1.5, 'Z') # Z轴标签 glPopMatrix() # if hasattr(self, 'model_scale'): # glScalef(self.model_scale, self.model_scale, self.model_scale) # def paintGLAxes(self): # """绘制坐标轴""" # glLineWidth(3.0) # glBegin(GL_LINES) # # X轴 - 红色 # glColor3f(1.0, 0.0, 0.0) # glVertex3f(0.0, 0.0, 0.0) # glVertex3f(100.0, 0.0, 0.0) # # # Y轴 - 绿色 # glColor3f(0.0, 1.0, 0.0) # glVertex3f(0.0, 0.0, 0.0) # glVertex3f(0.0, 100.0, 0.0) # # # Z轴 - 蓝色 # glColor3f(0.0, 0.0, 1.0) # glVertex3f(0.0, 0.0, 0.0) # glVertex3f(0.0, 0.0, 100.0) # glEnd() # # # 绘制文本标签 # glColor3f(1.0, 0.0, 0.0) # 设置颜色为红色 # self.renderText(1.5, 0, 0, 'X') # X轴标签 # # glColor3f(0.0, 1.0, 0.0) # 设置颜色为绿色 # self.renderText(0, 1.5, 0, 'Y') # Y轴标签 # # glColor3f(0.0, 0.0, 1.0) # 设置颜色为蓝色 # self.renderText(0, 0, 1.5, 'Z') # Z轴标签 def set_data(self, vertices, colors, triangles=None, normals=None): """设置 3D 数据(支持 mesh)""" self.vertices = np.array(vertices, dtype=np.float32) self.colors = np.array(colors, dtype=np.float32) if triangles is not None: self.triangles = np.array(triangles, dtype=np.int32) else: self.triangles = np.array([]) if normals is not None: self.normals = np.array(normals, dtype=np.float32) else: self.normals = np.array([]) print(f"✅ 设置数据: {len(self.vertices)} 个顶点, {len(self.triangles)} 个三角面") self.update() def mousePressEvent(self, event): self.last_mouse_pos = event.pos() def mouseMoveEvent(self, event): if self.last_mouse_pos is None: return dx = event.x() - self.last_mouse_pos.x() dy = event.y() - self.last_mouse_pos.y() is_ctrl = event.modifiers() & Qt.ControlModifier if event.buttons() & Qt.LeftButton: if is_ctrl: # Ctrl + 左键:旋转世界(视角) self.view_rotation[0] += dy * 0.5 # 俯仰 self.view_rotation[1] += dx * 0.5 # 偏航 self.view_rotation[0] = max(-89.0, min(89.0, self.view_rotation[0])) else: # 左键:旋转模型 self.model_rotation[0] += dy * 0.5 self.model_rotation[1] += dx * 0.5 elif event.buttons() & Qt.RightButton: if is_ctrl: # Ctrl + 右键:平移世界(视点平移) self.view_pan[0] += dx * 0.01 self.view_pan[1] -= dy * 0.01 else: # 右键:平移模型 self.model_pan[0] += dx * 0.01 self.model_pan[1] -= dy * 0.01 self.last_mouse_pos = event.pos() self.update() def wheelEvent(self, event): delta = event.angleDelta().y() self.view_distance -= delta * 0.05 self.view_distance = max(1.0, min(50.0, self.view_distance)) self.update() def toggle_display_mode(self): """切换显示模式:点云 <-> 表面""" if self.display_mode == 'points': self.display_mode = 'surface' else: self.display_mode = 'points' self.update() # 切换模式后重新绘制 def toggle_axes_display_mode(self): "切换坐标系显示模式" if self.axes_display_mode == False: self.axes_display_mode = True else: self.axes_display_mode = False self.update()