123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 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()
|