ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

three.quarks 移动端触摸粒子教程:3 个配方 + 60fps 参数表

three.quarks 移动端触摸粒子教程:3 个配方 + 60fps 参数表

three.quarks 移动端触摸粒子教程:3 个配方 + 60fps 参数表

【免费下载链接】three.quarksThree.quarks is a general purpose particle system / VFX engine for three.js项目地址: https://gitcode.com/GitHub_Trending/th/three.quarks

在手机上用 Three.js 做触摸粒子特效,绕不开两个问题:手指一动就要出效果,以及低端机不能被拖垮。这篇教程用 three.quarks 把这两件事拆开处理——先跑通点按爆发,再给滑动轨迹、多点触控各一套参数配方,最后附机型分档表和排查速查表,目标是让中端机上的触摸特效稳定在 60fps。

10 分钟跑通:点一下屏幕的粒子爆发

最小闭环只需三步:一个BatchedRenderer、一个挂在场景里的emitter、一次restart()。触摸坐标换算成场景内坐标后,移动发射器位置再重启系统,粒子就从指尖炸出来。

import { Scene, PerspectiveCamera, WebGLRenderer, TextureLoader } from 'three'; import { ParticleSystem, PointEmitter, ConstantValue, IntervalValue, BatchedParticleRenderer, } from 'three.quarks'; const scene = new Scene(); const camera = new PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100); const renderer = new WebGLRenderer({ antialias: false }); // 移动端关抗锯齿 renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); camera.position.z = 8; // 全局唯一:所有粒子系统都挂到它下面,按渲染管线自动合批 const batchRenderer = new BatchedParticleRenderer(); scene.add(batchRenderer); // 纹理 128~256px 足够,省显存 const texture = new TextureLoader().load('textures/texture1.png'); const ps = new ParticleSystem({ duration: 1, looping: false, autoDestroy: false, // 复用系统,不销毁 startLife: new IntervalValue(0.3, 0.5), startSpeed: new IntervalValue(1, 3), startSize: new ConstantValue(0.3), worldSpace: true, shape: new PointEmitter(), emissionOverTime: new ConstantValue(0), // 只靠 burst 出粒子 emissionBursts: [{ time: 0, count: new ConstantValue(24), cycle: 1, interval: 0.01, probability: 1 }], material: { map: texture, transparent: true }, }); batchRenderer.addSystem(ps); scene.add(ps.emitter); renderer.domElement.addEventListener('pointerdown', (e) => { const x = (e.clientX / innerWidth) * 2 - 1; const y = -(e.clientY / innerHeight) * 2 + 1; const k = -8 / Math.tan((camera.fov * Math.PI) / 360); // 换算系数 ps.emitter.position.set(x * k * 0.5, y * k * 0.5, 0); ps.restart(); // 每次触摸都重放 burst }); let last = performance.now(); (function tick(now) { const delta = Math.min((now - last) / 1000, 0.1); last = now; batchRenderer.update(delta); // 每帧推进所有系统 renderer.render(scene, camera); requestAnimationFrame(tick); })(last);

跑起来后,点按屏幕会有 24 个粒子向四周散开。worldSpace: true让粒子留在世界坐标里,不跟着发射器移动,爆发感才成立。

先搞懂三个核心件

three.quarks 里所有配置最终都落在三件事上:粒子从哪来、参数取什么值、怎么画到屏幕上。

核心件解决什么问题典型用法
Emitter 形状(PointEmitter、CircleEmitter…)决定粒子的出生位置和初速度方向shape: new PointEmitter()
Value 包装(ConstantValue/IntervalValue)决定每个粒子参数的固定值或随机区间,如寿命、速度、大小startLife: new IntervalValue(0.3, 0.5)
BatchedRenderer把同管线的系统合并成少数几次 draw call,并统一驱动更新batchRenderer.addSystem(ps)

发射器只负责"出生",出生之后的一切交给 behaviors。Value 包装是理解配置的关键:凡是startLife、startSpeed这类参数,都不直接写数字,而是包一层 Generator——ConstantValue(2)表示固定 2,IntervalValue(1, 3)表示每个粒子在 1 到 3 之间随机一次,且整个生命周期内保持同一随机值。这层设计让"一批粒子各不相同但各自稳定"成为默认行为。

效果配方:从点到轨迹再到多点

点按爆发:加形状和衰减

目标:让指尖的粒子更像火花而不是光点。关键参数:

  • shape换成ConeEmitter,给初速度一个锥形方向,爆发更有冲击感
  • SpeedOverLife把速度从 2.5 倍降到 0.2 倍,粒子先冲后坠
  • SizeOverLife配合PiecewiseBezier控制缩小曲线,最后 20% 寿命内快速消失
  • burst 数量控制在 24~40,手机上单点按超过 60 个粒子肉眼无差异,纯浪费
// 替换点按爆发里的三处配置 shape: new ConeEmitter({ radius: 0.1, angle: Math.PI / 6, thickness: 1, arc: Math.PI * 2 }), startSpeed: new IntervalValue(2, 4), ps.addBehavior(new SpeedOverLife(new PiecewiseBezier([ [new Bezier(2.5, 0.5, 0.2, 0.1), 0]]))); ps.addBehavior(new SizeOverLife(new PiecewiseBezier([ [new Bezier(1, 0.8, 0.3, 0), 0]])));

滑动轨迹:用距离发射

目标:手指划过屏幕时留下一条逐渐消散的粒子带。关键参数:

  • 轨迹效果的核心是emissionOverDistance——按发射器移动距离出粒子,而不是按时间。手指停住就不再产生粒子,轨迹自然断开
  • startLife压到 0.4s 左右,粒子要"短命",轨迹长度才跟得上手指速度
  • 系统设looping: true常驻运行,触摸时只更新emitter.position,不需要重启
  • 想更顺滑可以切RenderMode.Trail渲染,但 billboard 点粒子在手机上开销更小,优先用默认模式

const trail = new ParticleSystem({ duration: 5, looping: true, // 常驻,不随 duration 结束 startLife: new IntervalValue(0.25, 0.4), startSpeed: new ConstantValue(0.2), // 近静止,让轨迹由手指带动 startSize: new IntervalValue(0.1, 0.2), startColor: new ColorRange(new Vector4(0.0, 0.6, 1.0, 1), new Vector4(0.6, 0.5, 1.0, 1)), worldSpace: true, shape: new PointEmitter(), emissionOverTime: new ConstantValue(0), emissionOverDistance: new ConstantValue(12), // 每单位路程出 12 个粒子 material: { map: texture, transparent: true }, }); batchRenderer.addSystem(trail); scene.add(trail.emitter); // 触摸移动时只改位置,轨迹自动生成 renderer.domElement.addEventListener('pointermove', (e) => { if (e.buttons === 0 && e.pointerType === 'mouse') return; const x = (e.clientX / innerWidth) * 2 - 1; const y = -(e.clientY / innerHeight) * 2 + 1; const k = -8 / Math.tan((camera.fov * Math.PI) / 360); trail.emitter.position.x = x * k * 0.5; trail.emitter.position.y = y * k * 0.5; });

多点触控:系统池而不是临时 new

目标:两根手指同时划,各有一条独立轨迹,互不干扰。关键参数:

  • 不要每次触摸都new ParticleSystem——材质、几何体都会重建。预创建 4~5 个系统做池子,按下时领一个,抬起时归位
  • 用touch.identifier做键管理占用,touchend时立刻回收
  • 上限 5 个并发系统,再多对手机内存不友好,且人手实际不会同时按 6 个点

const pool = []; // 空闲系统 const active = new Map(); // identifier -> 系统 for (let i = 0; i < 4; i++) { // 池子大小即并发上限 const t = makeTrailSystem(texture); // 上面的 trail 配置封装成函数 batchRenderer.addSystem(t); scene.add(t.emitter); pool.push(t); } function onDown(e) { const t = pool.pop(); if (!t) return; // 池子空了直接放弃,不扩容 moveEmitter(t, e); t.restart(); active.set(e.pointerId, t); } function onMove(e) { const t = active.get(e.pointerId); if (t) moveEmitter(t, e); } function onUp(e) { const t = active.get(e.pointerId); if (t) { t.stop(); active.delete(e.pointerId); pool.push(t); } } // pointerdown / pointermove / pointerup 分别绑上面三个函数

机型分档与性能参数表

参数没有银弹,按机器分三档起步,再根据实测微调。

档位单系统粒子峰值发射率上限纹理策略
高(近 3 年旗舰)80060/s256px,可开软粒子
中(主流机型)30025/s128px,关闭softParticles
低(4GB 内存左右)12010/s64px,同时活跃系统 ≤ 3

阈值建议:连续 30 帧里平均帧耗超过 22ms,就降到下一档,只改数值不动结构:

function applyTier(tier, systems) { const t = { high: [24, 60], mid: [10, 25], low: [4, 10] }[tier]; // [burst, 发射率] for (const ps of systems) { ps.emissionBursts[0].count = new ConstantValue(t[0]); ps.emissionOverTime = new ConstantValue(t[1]); ps.startLife = new IntervalValue(0.25, 0.4); // 短命=低驻留 } } // 帧耗滑动窗口:每帧记录 performance.now() 差值,保留最近 30 帧 if (avg(last30) > 22) applyTier('low', allSystems);

它为什么快

draw call 是移动端粒子的头号杀手。three.quarks 的思路:一个场景只放一个BatchedRenderer,调用addSystem时按"渲染管线签名"(材质、混合模式、渲染模式、tile 数、renderOrder 等)比对现有批次,签名相同的系统直接并入同一个实例化批次,签名不同的才新建批次。100 个同管线的粒子系统最终可能只有 1~2 次 draw call。粒子死亡后其内存槽位被后续粒子复用,不触发新的内存分配。这套机制在 packages/three.quarks/src/BatchedRenderer.ts 里可以看到完整的分组与合并逻辑。

自定义粒子行为

内置 behaviors 覆盖不了手感时,写一个自己的。Behavior 就四个方法:initialize、update、frameUpdate、toJSON(外加clone/reset)。下面这个把粒子拉向手指位置,适合做"指尖吸附"的手感:

class PullToTouch { type = 'PullToTouch'; target = new Vector3(); strength = 4; tmp = new Vector3(); initialize() {} update(particle, delta) { // 每帧对每个粒子调用 this.tmp.copy(this.target).sub(particle.position); particle.velocity.addScaledVector(this.tmp, this.strength * delta); } frameUpdate() {} // 需要按帧推进的状态写这里 toJSON() { return { type: this.type, strength: this.strength }; } clone() { return Object.assign(Object.create(PullToTouch.prototype), this); } reset() {} } // 用法: const pull = new PullToTouch(); ps.addBehavior(pull); // 触摸 move 时:pull.target.copy(当前场景坐标)

update里只写纯数学、不分配对象,tmp复用临时向量,是保持帧率的关键习惯。

排查速查表

现象可能原因处理方式
点按后响应慢半拍忘了restart(),或系统还挂在旧位置每次触摸移动emitter.position后调用restart()
帧率掉到 30 以下单系统粒子超 500,或多系统不同材质导致批次增多按分档表降 burst 与发射率;统一material与blending
轨迹断断续续emissionOverDistance偏低,粒子寿命太短距离发射率提到 12~20;startLife下限提到 0.3
同一效果有的设备发灰有的发亮纹理色彩空间与blending配置不一致纹理统一sRGB,AdditiveBlending场景关闭色调映射影响

下一步

想直接看效果,进 packages/quarks.examples/ 目录,里面的每个 demo 文件(爆发、轨迹、软粒子、自定义插件)都可以本地跑起来对照参数;Value 包装的完整列表在 packages/quarks.core/src/functions/,发射器形状在 packages/quarks.core/src/shape/。

把上面三个配方拼在一起——点按爆发、滑动轨迹、多点池化——就是一个可上生产的移动端触摸特效模块。剩下的事只有一件:拿一台真实的中端机,把分档阈值跑一遍。

【免费下载链接】three.quarksThree.quarks is a general purpose particle system / VFX engine for three.js项目地址: https://gitcode.com/GitHub_Trending/th/three.quarks

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进