Qt Quick 粒子系统(三):发射器深度解析

目录

一、发射器是粒子系统的源头

粒子系统的所有视觉表现都始于 Emitter------它决定了粒子从哪里来、以什么频率出来、出来后往哪飞。如果把 ParticleSystem 比作一个舞台,Emitter 就是演员的入口;入口的大小、位置和开放方式,直接决定了演出的效果。

上一篇示例中展示了连续发射,但实际项目中的发射需求远不止这一种:爆炸效果需要"一次性涌出大量粒子",按钮点击反馈需要"点击后短暂发射",背景粒子需要"持续稳定输出"。这三种需求分别对应 Emitter 的三种发射模式:burst() 脉冲、pulse() 定时脉冲、emitRate 连续发射。

本文的目标是彻底理解这三种模式的行为差异、参数含义和适用场景,让你在面对任何发射需求时都能选择正确的方案。


二、开发环境与版本说明

本文所有代码基于以下环境验证(验证日期:2026-06-08):

  • Qt 版本 :6.8.2(最低要求 Qt 6.5,参见 CMakeLists.txt 中 qt_standard_project_setup(REQUIRES 6.5)
  • 编译器:MinGW 64-bit
  • 操作系统:Windows 11
  • 构建工具:CMake 3.29

三、原理分析:三种发射模式

3.1 emitRate 连续发射

emitRate 是 Emitter 最基础的属性,表示每秒发射的粒子数量。它的行为是:只要 enabledtrue 且 ParticleSystem 处于 running 状态,就按照固定频率持续发射粒子。

qml 复制代码
Emitter {
    emitRate: 80        // 每秒发射 80 个粒子
    lifeSpan: 2000      // 每个粒子存活 2 秒
    size: 12
}

稳态粒子数的估算 :当发射和消亡达到平衡时,屏幕上的活跃粒子数约为 emitRate × lifeSpan / 1000。上面的例子中,稳态粒子数约为 80 × 2000 / 1000 = 160 个。这个公式在调参时非常有用------如果你想让屏幕上有 500 个粒子,可以设 emitRate: 100, lifeSpan: 5000emitRate: 250, lifeSpan: 2000

emitRate 为 0 时 :Emitter 不会自动发射任何粒子,但仍可通过 burst() 手动触发。这在"只在交互时发射"的场景中很常见。

3.2 burst(count) 脉冲发射

burst(count) 在调用瞬间一次性发射指定数量的粒子,不依赖 emitRate。它适合需要"爆发感"的场景:爆炸、点击光效、烟花绽放。

qml 复制代码
Emitter {
    id: emitter
    emitRate: 0              // 默认不发射
    lifeSpan: 2000
    size: 12
    velocity: AngleDirection {
        angle: 0
        angleVariation: 360  // 全方向扩散
        magnitude: 100
    }
}

// 用户点击时触发
MouseArea {
    onClicked: emitter.burst(50)  // 一次性发射 50 个粒子
}

burst 的关键特征

  • 瞬时性:所有粒子在同一帧诞生,视觉上是"同时涌出"
  • 不干扰 emitRateburst()emitRate 互不影响,可以在连续发射的同时触发脉冲
  • 可重复调用:每次调用都追加新粒子,不会清除之前的粒子

3.3 pulse(duration) 定时脉冲

pulse(duration) 在 Emitter 未启用时,将其启用指定毫秒数,到期后自动关闭。它是 emitRateburst() 之间的折中方案------既有持续性,又有时间限制。注意:pulse() 只在 Emitter 当前未启用(enabled: false)时有效,如果 Emitter 已经启用,调用 pulse() 不会有额外效果。

qml 复制代码
Emitter {
    id: emitter
    emitRate: 200            // 脉冲期间每秒发射 200 个
    enabled: false           // 默认禁用
    lifeSpan: 2000
    size: 12
}

// 用户点击时触发
MouseArea {
    onClicked: emitter.pulse(500)  // 启用 500ms,期间按 emitRate: 200 发射
}

pulse 的关键特征

  • 渐进性 :粒子在 duration 毫秒内陆续出现,不像 burst() 那样同时涌出
  • 自动关闭pulse() 结束后,Emitter 自动恢复为 enabled: false
  • 受 emitRate 影响 :脉冲期间的发射量 = emitRate × duration / 1000

pulse() 的效果由 emitRateduration 共同决定。emitRate 控制粒子密度,duration 控制持续时间。调用 pulse(500) 时,Emitter 在 500ms 内按 emitRate 发射,到期后自动禁用。duration 太短(< 200ms)效果接近 burst(),太长(> 2000ms)就失去了"脉冲"的感觉。一般建议 300-1000ms。

3.4 三种模式对比

维度 emitRate 连续 burst(count) pulse(duration)
发射方式 持续均匀 瞬时爆发 限时持续
粒子出现节奏 陆续出现 同时涌出 陆续出现
是否受 emitRate 影响
是否自动停止 否(除非 disable) 是(一次完成) 是(duration 到期)
典型场景 背景粒子、持续效果 爆炸、点击反馈 短暂喷射、呼吸效果

用一张决策流程图来总结:
#mermaid-svg-XL6zsikvHzFp6hLy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XL6zsikvHzFp6hLy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XL6zsikvHzFp6hLy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XL6zsikvHzFp6hLy .error-icon{fill:#552222;}#mermaid-svg-XL6zsikvHzFp6hLy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XL6zsikvHzFp6hLy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XL6zsikvHzFp6hLy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XL6zsikvHzFp6hLy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XL6zsikvHzFp6hLy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XL6zsikvHzFp6hLy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XL6zsikvHzFp6hLy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XL6zsikvHzFp6hLy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XL6zsikvHzFp6hLy .marker.cross{stroke:#333333;}#mermaid-svg-XL6zsikvHzFp6hLy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XL6zsikvHzFp6hLy p{margin:0;}#mermaid-svg-XL6zsikvHzFp6hLy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XL6zsikvHzFp6hLy .cluster-label text{fill:#333;}#mermaid-svg-XL6zsikvHzFp6hLy .cluster-label span{color:#333;}#mermaid-svg-XL6zsikvHzFp6hLy .cluster-label span p{background-color:transparent;}#mermaid-svg-XL6zsikvHzFp6hLy .label text,#mermaid-svg-XL6zsikvHzFp6hLy span{fill:#333;color:#333;}#mermaid-svg-XL6zsikvHzFp6hLy .node rect,#mermaid-svg-XL6zsikvHzFp6hLy .node circle,#mermaid-svg-XL6zsikvHzFp6hLy .node ellipse,#mermaid-svg-XL6zsikvHzFp6hLy .node polygon,#mermaid-svg-XL6zsikvHzFp6hLy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XL6zsikvHzFp6hLy .rough-node .label text,#mermaid-svg-XL6zsikvHzFp6hLy .node .label text,#mermaid-svg-XL6zsikvHzFp6hLy .image-shape .label,#mermaid-svg-XL6zsikvHzFp6hLy .icon-shape .label{text-anchor:middle;}#mermaid-svg-XL6zsikvHzFp6hLy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XL6zsikvHzFp6hLy .rough-node .label,#mermaid-svg-XL6zsikvHzFp6hLy .node .label,#mermaid-svg-XL6zsikvHzFp6hLy .image-shape .label,#mermaid-svg-XL6zsikvHzFp6hLy .icon-shape .label{text-align:center;}#mermaid-svg-XL6zsikvHzFp6hLy .node.clickable{cursor:pointer;}#mermaid-svg-XL6zsikvHzFp6hLy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XL6zsikvHzFp6hLy .arrowheadPath{fill:#333333;}#mermaid-svg-XL6zsikvHzFp6hLy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XL6zsikvHzFp6hLy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XL6zsikvHzFp6hLy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XL6zsikvHzFp6hLy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XL6zsikvHzFp6hLy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XL6zsikvHzFp6hLy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XL6zsikvHzFp6hLy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XL6zsikvHzFp6hLy .cluster text{fill:#333;}#mermaid-svg-XL6zsikvHzFp6hLy .cluster span{color:#333;}#mermaid-svg-XL6zsikvHzFp6hLy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XL6zsikvHzFp6hLy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XL6zsikvHzFp6hLy rect.text{fill:none;stroke-width:0;}#mermaid-svg-XL6zsikvHzFp6hLy .icon-shape,#mermaid-svg-XL6zsikvHzFp6hLy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XL6zsikvHzFp6hLy .icon-shape p,#mermaid-svg-XL6zsikvHzFp6hLy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XL6zsikvHzFp6hLy .icon-shape .label rect,#mermaid-svg-XL6zsikvHzFp6hLy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XL6zsikvHzFp6hLy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XL6zsikvHzFp6hLy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XL6zsikvHzFp6hLy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 持续不断的粒子
一次性爆发大量粒子
短暂持续发射
你需要什么效果?
emitRate: N

(连续发射)
emitRate: 0 + burst(N)

(脉冲发射)
emitRate: N + pulse(duration)

(定时脉冲)
背景雪花、星空、水面波纹
爆炸、点击光效、烟花绽放
短暂喷射、呼吸灯、触发效果

四、代码实现:Concept_EmitterMethods.qml 逐段解析

项目中 Concept_EmitterMethods.qml 并排展示了 burst()pulse() 两种方法的效果对比。页面分为左右两栏,每栏各有一个粒子系统和一个触发按钮。

4.1 左栏:burst 脉冲发射

qml 复制代码
ParticleSystem {
    id: ps1
    anchors.fill: parent
    running: root.isCurrentItem

    ImageParticle {
        source: "qrc:/images/star.png"
        color: "#FF6B6B"
        alpha: 0.9
    }

    Emitter {
        id: burstEmitter
        anchors.centerIn: parent
        width: 1
        height: 1
        emitRate: 0
        lifeSpan: 2000
        size: 12
        velocity: AngleDirection {
            angle: 0
            angleVariation: 360
            magnitude: 100
        }
    }
}

emitRate: 0 ------Emitter 默认不发射任何粒子。只有当用户点击按钮调用 burst(50) 时才会产生粒子。这是 burst() 模式的典型写法:把 emitRate 设为 0,完全由手动触发控制。

width: 1, height: 1 ------Emitter 的发射区域设为 1×1 像素,近似一个点。配合 angleVariation: 360,粒子从中心点向四面八方均匀扩散,形成爆炸般的视觉效果。

触发逻辑

qml 复制代码
MouseArea {
    id: burstArea
    anchors.fill: parent
    hoverEnabled: true
    onClicked: burstEmitter.burst(50)
}

每次点击调用 burst(50),瞬间发射 50 个粒子。由于 lifeSpan: 2000,点击后 2 秒内粒子会逐渐消亡。如果在粒子消亡前再次点击,新旧粒子会叠加,形成更密集的效果。

4.2 右栏:pulse 定时脉冲

qml 复制代码
Emitter {
    id: pulseEmitter
    anchors.centerIn: parent
    width: 1
    height: 1
    emitRate: 200
    enabled: false
    lifeSpan: 2000
    size: 12
    velocity: AngleDirection {
        angle: 0
        angleVariation: 360
        magnitude: 100
    }
}

emitRate: 200, enabled: false ------与 burst 模式的关键区别:这里 emitRate 设为 200(脉冲期间每秒发射 200 个),但 enabled 默认为 false(不发射)。pulse(500) 会在 500ms 内临时启用 Emitter,期间按 emitRate: 200 发射,500ms 后自动恢复 enabled: false

触发逻辑

qml 复制代码
MouseArea {
    onClicked: pulseEmitter.pulse(500)
}

pulse(500) 意味着每次点击后,Emitter 在 500ms 内发射约 200 × 0.5 = 100 个粒子。与 burst(50) 相比,粒子不是同时涌出,而是在半秒内陆续出现,视觉上更柔和。

duration 选择 500ms 的理由:太短(如 200ms)效果接近 burst,太长(如 2000ms)就失去了"脉冲"的感觉。500ms 是一个能体现"陆续出现"特性的中间值。

4.3 为什么用两个独立的 ParticleSystem

左右两栏各自拥有独立的 ParticleSystemps1ps2),而不是共享一个。这是因为:

  • 粒子系统的坐标是相对于 ParticleSystem 容器的,两个独立系统可以各自在自己的区域内发射
  • 避免两个 Emitter 的粒子混在一起,影响对比效果
  • 更清晰地展示 burst()pulse() 的行为差异

五、运行效果

运行项目后,点击左侧导航栏的「发射器」进入本示例页面。页面分为左右两栏,并排展示 burst()pulse() 的效果对比。

左栏:burst 脉冲发射

点击红色圆形按钮触发 burst(50)。观察粒子的诞生方式:50 个红色星形粒子在同一帧同时从中心涌出,向四面八方均匀扩散(angleVariation: 360),形成"爆炸"般的瞬间扩散效果。2 秒后(lifeSpan: 2000)粒子逐渐消亡。

验证方式:连续快速点击多次,观察粒子叠加效果。由于 burst() 每次追加新粒子而不清除旧粒子,多次点击后屏幕上会同时存在多批粒子,密度明显增大。

右栏:pulse 定时脉冲

点击青色圆形按钮触发 pulse(500)。观察粒子的诞生方式:与 burst 不同,粒子不是同时涌出,而是在 500ms 内陆续从中心发射出来(emitRate: 200,500ms 内约发射 100 个粒子)。

验证方式:点击后观察粒子的出现节奏。前 500ms 内粒子持续涌出,500ms 后 Emitter 自动关闭(enabled 恢复为 false),不再有新粒子产生。已发射的粒子按 lifeSpan: 2000 自然消亡。

burst 与 pulse 的核心区别

两者的差异体现在粒子的诞生方式上:

  • burst:所有粒子在同一帧诞生,视觉上是"同时涌出"
  • pulse :粒子在 duration 毫秒内陆续诞生,视觉上是"逐渐流出"

六、适用边界与限制条件

burst()emitRate 可以同时使用burst() 是额外追加粒子,不影响 emitRate 的连续发射。比如设 emitRate: 20 作为背景粒子,同时在用户点击时 burst(50) 追加爆发效果。

pulse() 的前置条件pulse() 只在 Emitter 当前未启用(enabled: false)时有效。如果 Emitter 已经处于启用状态,调用 pulse() 不会有额外效果。pulse() 结束后,Emitter 自动恢复为 enabled: false

maximumEmitted 安全阀maximumEmitted 限制该 Emitter 的最大活跃粒子数。当活跃粒子数达到上限时,新的发射请求会被忽略。默认值为 -1(无限制)。在 burst() 大量发射时尤其有用,防止性能崩溃。

lifeSpan 的边界值:设为 0 时粒子立即消亡,视觉上看不到任何效果;设为 -1 时粒子永生(不会消亡),粒子数会无限增长直到性能崩溃。

emitRate 的性能上限 :设为极大值(如 10000)时,瞬间发射大量粒子可能导致帧率骤降。如果需要更密集的效果,优先增大 lifeSpan 而不是 emitRate------增大 lifeSpan 让粒子存活更久,自然积累更多;增大 emitRate 则会增加每秒的发射开销。


七、总结与下篇预告

本文讲解了 Emitter 的三种发射模式:

模式 核心特征 选择依据
emitRate 连续 持续均匀发射 需要不间断的粒子效果
burst(count) 瞬时大量发射 需要爆发感的一次性效果
pulse(duration) 限时持续发射 需要短暂但有持续性的效果

选择策略:持续效果用 emitRate,交互反馈用 burst,定时效果用 pulse,混合场景可以组合使用

下一篇将讲解粒子的外观渲染------ImageParticle 的 GPU 批量渲染能力和 ItemParticle 的 QML 组件渲染能力,以及如何在两者之间做出正确的选型。


资源下载qml_particlesystem ------ 包含完整的、可运行的代码

系列目录

相关推荐
用户8055336980312 小时前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner12 小时前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner9 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner9 天前
DicomViewer (添加模型类)3
qt
xcyxiner10 天前
DicomViewer (目录调整) 2
qt
xcyxiner10 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能12 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G12 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt