You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

QPainter伪3D挤出效果:侧面填充与透视穿透问题求解

伪3D等距挤出效果的问题解决

问题分析

  1. 初始版本无法填充侧面:仅通过drawLine连接对应顶点,未创建闭合的侧面路径,QPainter无法填充非闭合区域。
  2. 更新版本的透视穿透:绘制顺序错误,侧面或偏移路径被画在原路径前方,导致视觉穿透;同时未处理文本这类复杂路径的子路径(如字母孔洞),仅处理了主路径。

解决方案

核心修复点

  • 调整绘制顺序:按「最远面 → 侧面 → 最近面」的顺序绘制,利用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 12:27:01