粒子属性调整
This commit is contained in:
1 parent
0be0844f0f
commit
2ad0b76d11
6 files changed
+530
-49
No files matched your search
+150
-12
@@ -6,6 +6,12 @@ export type EmitMode = 'stream' | 'burst'
|
||||
export type EmitShape = 'point' | 'circle' | 'rect' | 'cone' | 'orbit'
|
||||
/** 混合模式 */
|
||||
export type Blend = 'normal' | 'add' | 'multiply' | 'screen'
|
||||
/** 粒子属性取值方式 */
|
||||
export type AttributeMode = 'fixed' | 'random' | 'curve'
|
||||
/** 初始旋转额外支持跟随发射方向 */
|
||||
export type RotationMode = AttributeMode | 'direction'
|
||||
export type ScaleAxisMode = 'uniform' | 'separate'
|
||||
export interface CurvePoint { x: number; y: number }
|
||||
|
||||
/** 粒子配置(引用保持,面板改字段即实时生效) */
|
||||
export interface EmitterConfig {
|
||||
@@ -39,15 +45,47 @@ export interface EmitterConfig {
|
||||
// 属性
|
||||
lifeMin: number
|
||||
lifeMax: number
|
||||
lifeMode: AttributeMode
|
||||
lifeCurve: CurvePoint[]
|
||||
speedMin: number
|
||||
speedMax: number
|
||||
speedMode: AttributeMode
|
||||
speedCurve: CurvePoint[]
|
||||
scaleMin: number
|
||||
scaleMax: number
|
||||
scaleMode: AttributeMode
|
||||
scaleAxisMode: ScaleAxisMode
|
||||
scaleYMin: number
|
||||
scaleYMax: number
|
||||
scaleCurve: CurvePoint[]
|
||||
scaleYCurve: CurvePoint[]
|
||||
scaleEndRatio: number
|
||||
initialRotationMode: RotationMode
|
||||
initialRotationMin: number
|
||||
initialRotationMax: number
|
||||
initialRotationCurve: CurvePoint[]
|
||||
alpha: number
|
||||
alphaEnd: number
|
||||
rotationSpeedMin: number
|
||||
rotationSpeedMax: number
|
||||
// 生命周期内属性
|
||||
speedOverLifeEnabled: boolean
|
||||
speedOverLifeMode: AttributeMode
|
||||
speedOverLifeMin: number
|
||||
speedOverLifeMax: number
|
||||
speedOverLifeCurve: CurvePoint[]
|
||||
scaleOverLifeEnabled: boolean
|
||||
scaleOverLifeMode: AttributeMode
|
||||
scaleOverLifeAxisMode: ScaleAxisMode
|
||||
scaleOverLifeMin: number
|
||||
scaleOverLifeMax: number
|
||||
scaleYOverLifeMin: number
|
||||
scaleYOverLifeMax: number
|
||||
scaleOverLifeCurve: CurvePoint[]
|
||||
scaleYOverLifeCurve: CurvePoint[]
|
||||
rotationOverLifeEnabled: boolean
|
||||
rotationOverLifeMode: AttributeMode
|
||||
rotationOverLifeCurve: CurvePoint[]
|
||||
// 物理
|
||||
gravity: number
|
||||
/** 阻尼(速度衰减系数,0=无) */
|
||||
@@ -140,15 +178,46 @@ export function defaultConfig(): EmitterConfig {
|
||||
coneAngle: 60,
|
||||
lifeMin: 0.6,
|
||||
lifeMax: 1.8,
|
||||
lifeMode: 'random',
|
||||
lifeCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
speedMin: 30,
|
||||
speedMax: 140,
|
||||
speedMode: 'random',
|
||||
speedCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
scaleMin: 0.3,
|
||||
scaleMax: 0.8,
|
||||
scaleMode: 'random',
|
||||
scaleAxisMode: 'uniform',
|
||||
scaleYMin: 0.3,
|
||||
scaleYMax: 0.8,
|
||||
scaleCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
scaleYCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
scaleEndRatio: 0.3,
|
||||
initialRotationMode: 'random',
|
||||
initialRotationMin: 0,
|
||||
initialRotationMax: 360,
|
||||
initialRotationCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
alpha: 0.9,
|
||||
alphaEnd: 0,
|
||||
rotationSpeedMin: -180,
|
||||
rotationSpeedMax: 180,
|
||||
speedOverLifeEnabled: false,
|
||||
speedOverLifeMode: 'fixed',
|
||||
speedOverLifeMin: 1,
|
||||
speedOverLifeMax: 1,
|
||||
speedOverLifeCurve: [{ x: 0, y: 1 }, { x: 1, y: 1 }],
|
||||
scaleOverLifeEnabled: false,
|
||||
scaleOverLifeMode: 'curve',
|
||||
scaleOverLifeAxisMode: 'uniform',
|
||||
scaleOverLifeMin: 0,
|
||||
scaleOverLifeMax: 1,
|
||||
scaleYOverLifeMin: 0,
|
||||
scaleYOverLifeMax: 1,
|
||||
scaleOverLifeCurve: [{ x: 0, y: 1 }, { x: 1, y: 1 }],
|
||||
scaleYOverLifeCurve: [{ x: 0, y: 1 }, { x: 1, y: 1 }],
|
||||
rotationOverLifeEnabled: false,
|
||||
rotationOverLifeMode: 'random',
|
||||
rotationOverLifeCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
|
||||
gravity: 0,
|
||||
damping: 0,
|
||||
seed: 0,
|
||||
@@ -182,6 +251,19 @@ export function defaultConfig(): EmitterConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/** 为热更新或旧版配置补齐后来新增的字段,避免 undefined 进入粒子计算。 */
|
||||
export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
|
||||
const defaults = defaultConfig() as unknown as Record<string, unknown>
|
||||
const target = config as unknown as Record<string, unknown>
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
if (target[key] !== undefined && target[key] !== null) continue
|
||||
target[key] = Array.isArray(value)
|
||||
? value.map((item) => typeof item === 'object' && item !== null ? { ...item } : item)
|
||||
: value
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
interface Particle {
|
||||
sprite: Sprite
|
||||
active: boolean
|
||||
@@ -190,9 +272,12 @@ interface Particle {
|
||||
maxLife: number
|
||||
x: number; y: number
|
||||
vx: number; vy: number
|
||||
scaleStart: number; scaleEnd: number
|
||||
scaleStartX: number; scaleStartY: number
|
||||
alphaStart: number; alphaEnd: number
|
||||
rotation: number; rotationSpeed: number
|
||||
speedScale: number
|
||||
speedScaleTarget: number
|
||||
scaleOverLifeX: number; scaleOverLifeY: number
|
||||
}
|
||||
|
||||
const BLEND_MAP: Record<Blend, number> = { normal: 0, add: 1, multiply: 2, screen: 3 }
|
||||
@@ -215,7 +300,7 @@ export class ParticleEmitter extends Container {
|
||||
|
||||
constructor(config: EmitterConfig, maxParticles = MAX_PARTICLES) {
|
||||
super()
|
||||
this.cfg = config
|
||||
this.cfg = ensureEmitterConfig(config)
|
||||
this._ensurePool(maxParticles)
|
||||
}
|
||||
|
||||
@@ -241,7 +326,8 @@ export class ParticleEmitter extends Container {
|
||||
this.pool.push({
|
||||
sprite, active: false, boneName: 'p_' + this.pool.length,
|
||||
life: 0, maxLife: 1, x: 0, y: 0, vx: 0, vy: 0,
|
||||
scaleStart: 1, scaleEnd: 1, alphaStart: 1, alphaEnd: 1, rotation: 0, rotationSpeed: 0,
|
||||
scaleStartX: 1, scaleStartY: 1, alphaStart: 1, alphaEnd: 1, rotation: 0, rotationSpeed: 0,
|
||||
speedScale: 1, speedScaleTarget: 1, scaleOverLifeX: 1, scaleOverLifeY: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -250,7 +336,14 @@ export class ParticleEmitter extends Container {
|
||||
|
||||
/** 每帧更新;返回活动粒子状态快照(与骨骼一一对应) */
|
||||
update(dt: number): ParticleState[] {
|
||||
// HMR 会保留已创建的系统对象;旧对象缺少新增字段时在这里即时补齐。
|
||||
const cfg = this.cfg
|
||||
if (
|
||||
cfg.lifeMode == null || cfg.speedMode == null || cfg.scaleMode == null ||
|
||||
cfg.initialRotationMode == null || cfg.speedOverLifeEnabled == null ||
|
||||
cfg.scaleOverLifeEnabled == null || cfg.rotationOverLifeEnabled == null ||
|
||||
!Array.isArray(cfg.scaleOverLifeCurve)
|
||||
) ensureEmitterConfig(cfg)
|
||||
this.reseed() // 种子变化即重建随机源
|
||||
// 累计发射时间
|
||||
this._elapsed += dt
|
||||
@@ -298,11 +391,29 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
p.x += p.vx * dt
|
||||
p.y += p.vy * dt
|
||||
p.rotation += p.rotationSpeed * dt
|
||||
const t = 1 - p.life / p.maxLife
|
||||
const s = p.sprite
|
||||
|
||||
if (cfg.speedOverLifeEnabled) {
|
||||
const target = finite(overLifeValue(cfg.speedOverLifeMode, cfg.speedOverLifeMin, cfg.speedOverLifeMax, cfg.speedOverLifeCurve, t, p.speedScaleTarget), 1)
|
||||
const ratio = p.speedScale === 0 ? target : target / p.speedScale
|
||||
p.vx *= ratio; p.vy *= ratio; p.speedScale = target
|
||||
}
|
||||
|
||||
let rotationSpeed = p.rotationSpeed
|
||||
if (cfg.rotationOverLifeEnabled && cfg.rotationOverLifeMode === 'curve') {
|
||||
rotationSpeed = lerp(cfg.rotationSpeedMin, cfg.rotationSpeedMax, curveAt(cfg.rotationOverLifeCurve, t))
|
||||
}
|
||||
p.rotation += rotationSpeed * dt
|
||||
s.x = p.x; s.y = p.y; s.rotation = -p.rotation * Math.PI / 180
|
||||
s.scale.set(p.scaleStart + (p.scaleEnd - p.scaleStart) * t)
|
||||
let scaleMulX = 1, scaleMulY = 1
|
||||
if (cfg.scaleOverLifeEnabled) {
|
||||
scaleMulX = overLifeValue(cfg.scaleOverLifeMode, cfg.scaleOverLifeMin, cfg.scaleOverLifeMax, cfg.scaleOverLifeCurve, t, p.scaleOverLifeX)
|
||||
scaleMulY = cfg.scaleOverLifeAxisMode === 'separate'
|
||||
? overLifeValue(cfg.scaleOverLifeMode, cfg.scaleYOverLifeMin, cfg.scaleYOverLifeMax, cfg.scaleYOverLifeCurve, t, p.scaleOverLifeY)
|
||||
: scaleMulX
|
||||
}
|
||||
s.scale.set(finite(p.scaleStartX * scaleMulX, 1), finite(p.scaleStartY * scaleMulY, 1))
|
||||
s.alpha = cfg.alphaMode === 'curve' ? curveAt(cfg.alphaCurve, t) * cfg.alphaScale : (p.alphaStart + (p.alphaEnd - p.alphaStart) * t) * cfg.alphaScale
|
||||
const rgb = cfg.colorOverLife ? blendRgb(cfg.colorStart, cfg.colorEnd, t) : hexToNumber(cfg.colorStart)
|
||||
s.tint = rgb
|
||||
@@ -340,8 +451,8 @@ export class ParticleEmitter extends Container {
|
||||
let idx = this.pool.findIndex((p) => !p.active)
|
||||
if (idx === -1) return // 池满
|
||||
const { x: ox, y: oy } = this.spawnOrigin()
|
||||
const life = lerp(cfg.lifeMin, cfg.lifeMax, this.rng())
|
||||
const speed = lerp(cfg.speedMin, cfg.speedMax, this.rng())
|
||||
const life = initialValue(cfg.lifeMode, cfg.lifeMin, cfg.lifeMax, cfg.lifeCurve, this.rng())
|
||||
const speed = initialValue(cfg.speedMode, cfg.speedMin, cfg.speedMax, cfg.speedCurve, this.rng())
|
||||
// 锥体:发射方向用锥形开口角度(coneAngle)散布,且 y 朝数学上正(与扇区绘制一致);其余形状沿用 spread + 屏幕坐标
|
||||
let dirAng: number
|
||||
if (cfg.shape === 'cone') {
|
||||
@@ -354,10 +465,27 @@ export class ParticleEmitter extends Container {
|
||||
p.x = ox; p.y = oy
|
||||
p.vx = Math.cos(dirAng) * speed
|
||||
p.vy = (cfg.shape === 'cone' ? -Math.sin(dirAng) : Math.sin(dirAng)) * speed
|
||||
p.scaleStart = lerp(cfg.scaleMin, cfg.scaleMax, this.rng())
|
||||
p.scaleEnd = p.scaleStart * cfg.scaleEndRatio
|
||||
p.scaleStartX = initialValue(cfg.scaleMode, cfg.scaleMin, cfg.scaleMax, cfg.scaleCurve, this.rng())
|
||||
p.scaleStartY = cfg.scaleAxisMode === 'separate'
|
||||
? initialValue(cfg.scaleMode, cfg.scaleYMin, cfg.scaleYMax, cfg.scaleYCurve, this.rng())
|
||||
: p.scaleStartX
|
||||
p.alphaStart = cfg.alpha; p.alphaEnd = cfg.alphaEnd
|
||||
p.rotation = this.rng() * 360; p.rotationSpeed = lerp(cfg.rotationSpeedMin, cfg.rotationSpeedMax, this.rng())
|
||||
p.rotation = cfg.initialRotationMode === 'direction'
|
||||
? -Math.atan2(p.vy, p.vx) * 180 / Math.PI + cfg.initialRotationMin
|
||||
: initialValue(cfg.initialRotationMode, cfg.initialRotationMin, cfg.initialRotationMax, cfg.initialRotationCurve, this.rng())
|
||||
p.rotationSpeed = cfg.rotationOverLifeEnabled
|
||||
? initialValue(cfg.rotationOverLifeMode, cfg.rotationSpeedMin, cfg.rotationSpeedMax, cfg.rotationOverLifeCurve, this.rng())
|
||||
: 0
|
||||
p.speedScale = 1
|
||||
p.speedScaleTarget = cfg.speedOverLifeMode === 'random'
|
||||
? lerp(cfg.speedOverLifeMin, cfg.speedOverLifeMax, this.rng())
|
||||
: cfg.speedOverLifeMin
|
||||
p.scaleOverLifeX = cfg.scaleOverLifeMode === 'random'
|
||||
? lerp(cfg.scaleOverLifeMin, cfg.scaleOverLifeMax, this.rng())
|
||||
: cfg.scaleOverLifeMin
|
||||
p.scaleOverLifeY = cfg.scaleOverLifeMode === 'random'
|
||||
? lerp(cfg.scaleYOverLifeMin, cfg.scaleYOverLifeMax, this.rng())
|
||||
: cfg.scaleYOverLifeMin
|
||||
p.sprite.tint = hexToNumber(cfg.colorStart)
|
||||
p.sprite.visible = true
|
||||
}
|
||||
@@ -421,9 +549,19 @@ export class ParticleEmitter extends Container {
|
||||
}
|
||||
|
||||
function lerp(a: number, b: number, t: number) { return a + (b - a) * t }
|
||||
function finite(value: number, fallback: number) { return Number.isFinite(value) ? value : fallback }
|
||||
function initialValue(mode: AttributeMode, min: number, max: number, curve: CurvePoint[], random: number) {
|
||||
if (mode === 'fixed') return min
|
||||
if (mode === 'curve') return lerp(min, max, curveAt(curve, random))
|
||||
return lerp(min, max, random)
|
||||
}
|
||||
function overLifeValue(mode: AttributeMode, min: number, max: number, curve: CurvePoint[], age: number, sampled: number) {
|
||||
if (mode === 'curve') return lerp(min, max, curveAt(curve, age))
|
||||
return mode === 'random' ? sampled : min
|
||||
}
|
||||
/** 在透明度曲线关键帧上按 x(生命周期时刻 0~1)求 y(透明度 0~1)。保证有序、端点外取端点 */
|
||||
function curveAt(pts: { x: number; y: number }[], x: number): number {
|
||||
if (!pts || pts.length === 0) return 1
|
||||
function curveAt(pts: CurvePoint[] | undefined, x: number): number {
|
||||
if (!Array.isArray(pts) || pts.length === 0) return 1
|
||||
const p = [...pts].sort((a, b) => a.x - b.x)
|
||||
if (x <= p[0].x) return p[0].y
|
||||
if (x >= p[p.length - 1].x) return p[p.length - 1].y
|
||||
|
||||
Reference in new issue
Block a user