子发射器

This commit is contained in:
tianmo committed 2026-10-22 16:49:53 +08:00
1 parent d4ebb18777
commit d224939efd
33 files changed
+5690 -1355

No files matched your search

+42 -23
View File
@@ -37,6 +37,7 @@ import { Application, Container, Graphics, type Text, type Texture } from 'pixi.
import { ParticleEmitter, ensureEmitterConfig, type ParticleState } from '../core/particleEmitter'
import { bakeLoopFrames } from '../core/loopFrameBaker'
import { loopPlaybackRange } from '../core/loopSegmentTiming'
import { collectSubEmitterIds } from '../core/subEmitterIds'
import { useParticleStore, type CollisionBody, type ParticleSystem } from '../store/particleStore'
import { SpineRuntimeLayer } from '../spine/SpineRuntimeLayer'
import { installEditorHistory, undoLastOperation } from '../editor/editorHistory'
@@ -220,12 +221,21 @@ function bakeSystemFrames(system: ParticleSystem, emitter: ParticleEmitter, fps:
*/
function ensureFramesBaked(fps: number) {
const totalFrames = Math.max(2, Math.floor(store.timeline.totalFrames) || 2)
const subEmitterIds = collectSubEmitterIds(store.systems)
let complete = true
for (const system of store.systems) {
if ((system.frames?.length || 0) >= totalFrames) continue
const emitter = emitterSync.emitters.get(system.id)
if (!emitter) { complete = false; continue }
bakeSystemFrames(system as unknown as ParticleSystem, emitter, fps, totalFrames)
// 烘焙预热会大量出生/死亡粒子,期间暂停跨系统事件,避免注入预热粒子。
emitterSync.suspendEvents(true)
try {
for (const system of store.systems) {
// 子发射器由父粒子事件驱动,不能脱离父系统单独烘焙。
if (subEmitterIds.has(system.id)) continue
if ((system.frames?.length || 0) >= totalFrames) continue
const emitter = emitterSync.emitters.get(system.id)
if (!emitter) { complete = false; continue }
bakeSystemFrames(system as unknown as ParticleSystem, emitter, fps, totalFrames)
}
} finally {
emitterSync.suspendEvents(false)
}
if (complete) store.timeline.recorded = true
}
@@ -351,6 +361,7 @@ function loop() {
appendStateBones(collected, state, emitter.position.x, emitter.position.y)
}
const tl = store.timeline
const playbackRange = store.playbackRange
const fps = tl.fps
const frameDuration = 1 / Math.max(1, fps)
let displayFrame = timelineDisplayFrame
@@ -376,18 +387,26 @@ function loop() {
total = 0
if (!tl.recorded) {
// 录制阶段:固定步长模拟并记录当前帧。
for (const sys of store.systems) {
const emitter = emitterSync.emitters.get(sys.id)
if (!emitter) continue
const isLoopStream = sys.config.mode === 'stream' && sys.config.streamBehavior === 'loop'
// 循环持续是一次性烘焙:只要缓存还没有就补上。录制不一定从第 0 帧开始
// (播放头可能被播放范围钳到 A 帧),只按 tl.frame === 0 判断会漏烘焙,
// 导致整段回放都没有可用帧。
if (isLoopStream) {
if (!sys.frames?.length) bakeLoopFrames(sys as unknown as ParticleSystem, emitter, fps, tl.totalFrames)
} else if (tl.frame === 0) {
emitter.reset()
emitterSync.suspendEvents(true)
try {
const subEmitterIds = collectSubEmitterIds(store.systems)
for (const sys of store.systems) {
const emitter = emitterSync.emitters.get(sys.id)
if (!emitter) continue
const isLoopStream = !subEmitterIds.has(sys.id)
&& sys.config.mode === 'stream'
&& sys.config.streamBehavior === 'loop'
// 循环持续是一次性烘焙:只要缓存还没有就补上。录制不一定从第 0 帧开始
// (播放头可能被播放范围钳到 A 帧),只按 tl.frame === 0 判断会漏烘焙,
// 导致整段回放都没有可用帧。
if (isLoopStream) {
if (!sys.frames?.length) bakeLoopFrames(sys as unknown as ParticleSystem, emitter, fps, tl.totalFrames)
} else if (tl.frame === 0) {
emitter.reset()
}
}
} finally {
emitterSync.suspendEvents(false)
}
for (const [systemId, em] of emitterSync.emitters) {
const system = store.systems.find((item) => item.id === systemId)
@@ -412,7 +431,7 @@ function loop() {
}
tl.frame++
if (tl.frame >= tl.totalFrames) {
tl.frame = tl.playbackStart
tl.frame = playbackRange.start
tl.recorded = true
}
} else {
@@ -435,13 +454,13 @@ function loop() {
: null
// 烘焙数据保留“末帧 = 首帧”以便导出闭环。预览精确循环段时,末帧只作为
// 数学边界,不再与下一轮首帧连续显示两次,避免衔接处停顿一帧。
const closesActiveLoop = tl.loop
&& activeLoopRange?.start === tl.playbackStart
&& activeLoopRange.end === tl.playbackEnd
const playbackLastFrame = closesActiveLoop ? tl.playbackEnd - 1 : tl.playbackEnd
const closesActiveLoop = tl.loop && tl.abLoop
&& activeLoopRange?.start === playbackRange.start
&& activeLoopRange.end === playbackRange.end
const playbackLastFrame = closesActiveLoop ? playbackRange.end - 1 : playbackRange.end
if (tl.frame > playbackLastFrame) {
if (tl.loop) tl.frame = tl.playbackStart
else { tl.frame = tl.playbackEnd; tl.playing = false; break }
if (tl.loop) tl.frame = playbackRange.start
else { tl.frame = playbackRange.end; tl.playing = false; break }
}
}
}