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 ------ 包含完整的、可运行的代码

系列目录

相关推荐
San813_LDD17 小时前
[QT]《Qt 开发避坑指南:随机数、容器操作与 VS 环境配置》
开发语言·qt
稷下元歌1 天前
七天学会plc加机器视觉之AI 接入 外设模块开发全详细操作文档(全程配套视频按文档实操)
python·sql·qt·贪心算法·r语言·wpf·时序数据库
艾莉丝努力练剑1 天前
【QT】界面优化:QSS
linux·运维·开发语言·网络·qt·计算机网络·udp
雪的季节1 天前
Qt 自定义表头
开发语言·qt
艾莉丝努力练剑1 天前
【QT】系统相关:QT文件
linux·服务器·开发语言·网络·qt·tcp/ip·计算机网络
爱思考的小伙1 天前
Qt-03:串口助手
qt
864记忆2 天前
远程执行指令-常用指令集
qt
郝学胜_神的一滴2 天前
Qt 高级开发 026:QTabWidget御道,从筑基到化境
c++·qt
走好每一步2 天前
2、VDK 使用QVTKOpenGLNativeWidget嵌入到QT窗体中
qt·vtk·三维图像