QPainter伪3D挤出效果:侧面填充与透视穿透问题求解
伪3D等距挤出效果的问题解决
问题分析
- 初始版本无法填充侧面:仅通过
drawLine连接对应顶点,未创建闭合的侧面路径,QPainter无法填充非闭合区域。 - 更新版本的透视穿透:绘制顺序错误,侧面或偏移路径被画在原路径前方,导致视觉穿透;同时未处理文本这类复杂路径的子路径(如字母孔洞),仅处理了主路径。
解决方案
核心修复点
- 调整绘制顺序:按「最远面 → 侧面 → 最近面」的顺序绘制,利用QPainter"后绘制覆盖先绘制"的特性,让近处的面遮挡远处区域,解决穿透问题。
- 处理所有子路径:针对文本等包含孔洞的复杂路径,遍历所有子路径生成多边形,避免遗漏侧面。
- 循环连接顶点:用模运算让最后一个顶点连接回第一个,确保侧面路径完全闭合。
修改后的完整代码
矩形挤出基础版
from PyQt6.QtGui import QPainter, QPainterPath, QPen, QBrush, QColor from PyQt6.QtCore import QPointF, Qt from PyQt6.QtWidgets import QApplication, QWidget, QSlider, QVBoxLayout import sys import math class ExtrudeWidget(QWidget): def __init__(self): super().__init__() self.resize(800, 300) # 创建矩形路径 self.path = QPainterPath() self.path.addRect(200, 100, 200, 50) # 挤出参数 self.extrusion_length = 15 self.extrusion_angle = 135 # UI布局 layout = QVBoxLayout() self.length_slider = QSlider(range(0,101), value=self.extrusion_length, tickInterval=1) self.length_slider.valueChanged.connect(lambda v: setattr(self, 'extrusion_length', v) or self.update()) layout.addWidget(self.length_slider) self.angle_slider = QSlider(range(0,361), value=self.extrusion_angle, tickInterval=1) self.angle_slider.valueChanged.connect(lambda v: setattr(self, 'extrusion_angle', v) or self.update()) layout.addWidget(self.angle_slider) self.setLayout(layout) def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) # 计算偏移量 angle_rad = math.radians(self.extrusion_angle) offset_x = self.extrusion_length * math.cos(angle_rad) offset_y = self.extrusion_length * math.sin(angle_rad) # 生成偏移路径 duplicated_path = QPainterPath(self.path) duplicated_path.translate(offset_x, offset_y) # 获取多边形顶点 original_poly = self.path.toFillPolygon(painter.worldTransform()) duplicated_poly = duplicated_path.toFillPolygon(painter.worldTransform()) orig_points = [(p.x(), p.y()) for p in original_poly] dup_points = [(p.x(), p.y()) for p in duplicated_poly] # 画笔设置 pen = QPen(QColor("black"), 1, joinStyle=Qt.PenJoinStyle.RoundJoin, capStyle=Qt.PenCapStyle.RoundCap) painter.setPen(pen) main_brush = QBrush(QColor("#ebd086")) side_brush = QBrush(QColor("#c4a56a")) # 1. 绘制最远面(偏移后的路径) painter.fillPath(duplicated_path, main_brush) painter.drawPath(duplicated_path) # 2. 绘制侧面 num_points = min(len(orig_points), len(dup_points)) for i in range(num_points): orig_p1 = QPointF(*orig_points[i]) orig_p2 = QPointF(*orig_points[(i+1)%num_points]) dup_p1 = QPointF(*dup_points[i]) dup_p2 = QPointF(*dup_points[(i+1)%num_points]) # 创建闭合侧面路径 side_face = QPainterPath() side_face.moveTo(orig_p1) side_face.lineTo(orig_p2) side_face.lineTo(dup_p2) side_face.lineTo(dup_p1) side_face.closeSubpath() painter.fillPath(side_face, side_brush) painter.drawPath(side_face) # 3. 绘制最近面(原路径),遮挡被覆盖区域 painter.fillPath(self.path, main_brush) painter.drawPath(self.path) if __name__ == "__main__": app = QApplication(sys.argv) window = ExtrudeWidget() window.show() sys.exit(app.exec())
文本挤出复杂路径版
from PyQt6.QtGui import QPainter, QPainterPath, QFont, QPen, QBrush, QColor from PyQt6.QtCore import QPointF, Qt from PyQt6.QtWidgets import QApplication, QWidget, QSlider, QVBoxLayout import sys import math class TextExtrudeWidget(QWidget): def __init__(self): super().__init__() self.resize(800, 300) # 创建文本路径 self.font = QFont("Arial", 120, QFont.Weight.Bold) self.path = QPainterPath() self.path.addText(100, 200, self.font, "HELLO!") # 挤出参数 self.extrusion_length = 25 self.extrusion_angle = 45 # UI布局 layout = QVBoxLayout() self.length_slider = QSlider(range(0,101), value=self.extrusion_length, tickInterval=1) self.length_slider.valueChanged.connect(lambda v: setattr(self, 'extrusion_length', v) or self.update()) layout.addWidget(self.length_slider) self.angle_slider = QSlider(range(0,361), value=self.extrusion_angle, tickInterval=1) self.angle_slider.valueChanged.connect(lambda v: setattr(self, 'extrusion_angle', v) or self.update()) layout.addWidget(self.angle_slider) self.setLayout(layout) def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) # 计算偏移量 angle_rad = math.radians(self.extrusion_angle) offset_x = self.extrusion_length * math.cos(angle_rad) offset_y = self.extrusion_length * math.sin(angle_rad) # 生成偏移路径 duplicated_path = QPainterPath(self.path) duplicated_path.translate(offset_x, offset_y) # 获取所有子路径的多边形(处理文本孔洞) def get_subpath_polygons(path, painter): polygons = [] for i in range(path.subpathCount()): subpath = QPainterPath() start_elem = path.elementAt(i) subpath.moveTo(start_elem.x, start_elem.y) elem_idx = i + 1 while elem_idx < path.elementCount() and not path.elementAt(elem_idx).isMoveTo(): elem = path.elementAt(elem_idx) if elem.isLineTo(): subpath.lineTo(elem.x, elem.y) elif elem.isCurveTo(): c1 = path.elementAt(elem_idx) c2 = path.elementAt(elem_idx+1) end = path.elementAt(elem_idx+2) subpath.cubicTo(c1.x, c1.y, c2.x, c2.y, end.x, end.y) elem_idx += 2 elem_idx += 1 subpath.closeSubpath() polygons.append(subpath.toFillPolygon(painter.worldTransform())) return polygons original_polygons = get_subpath_polygons(self.path, painter) duplicated_polygons = get_subpath_polygons(duplicated_path, painter) # 画笔设置 pen = QPen(QColor("black"), 1.4, joinStyle=Qt.PenJoinStyle.RoundJoin, capStyle=Qt.PenCapStyle.RoundCap) painter.setPen(pen) main_brush = QBrush(QColor("#ebd086")) side_brush = QBrush(QColor("#c4a56a")) # 1. 绘制最远面(偏移后的文本路径) painter.fillPath(duplicated_path, main_brush) painter.drawPath(duplicated_path) # 2. 绘制所有子路径的侧面 for orig_poly, dup_poly in zip(original_polygons, duplicated_polygons): orig_points = [(p.x(), p.y()) for p in orig_poly] dup_points = [(p.x(), p.y()) for p in dup_poly] num_points = min(len(orig_points), len(dup_points)) for i in range(num_points): orig_p1 = QPointF(*orig_points[i]) orig_p2 = QPointF(*orig_points[(i+1)%num_points]) dup_p1 = QPointF(*dup_points[i]) dup_p2 = QPointF(*dup_points[(i+1)%num_points]) side_face = QPainterPath() side_face.moveTo(orig_p1) side_face.lineTo(orig_p2) side_face.lineTo(dup_p2) side_face.lineTo(dup_p1) side_face.closeSubpath() painter.fillPath(side_face, side_brush) painter.drawPath(side_face) # 3. 绘制最近面(原文本路径) painter.fillPath(self.path, main_brush) painter.drawPath(self.path) if __name__ == "__main__": app = QApplication(sys.argv) window = TextExtrudeWidget() window.show() sys.exit(app.exec())
关键说明
- 绘制顺序逻辑:先画偏移路径(视觉最远),再画侧面,最后画原路径(视觉最近),彻底解决透视穿透问题。
- 子路径处理:通过遍历
subpathCount()获取文本的所有子路径(包括字母内部孔洞),确保每个部分都生成正确的侧面。 - 闭合侧面路径:使用
(i+1)%num_points实现顶点循环连接,保证侧面路径完全闭合,QPainter可正常填充。
内容的提问来源于stack exchange,提问作者Edward
相关产品推荐
相关产品推荐

