优化碰撞识别,加入暖场帧数

This commit is contained in:
tianmo committed 2026-10-29 17:20:05 +08:00
1 parent b8652acce0
commit fed1015849
21 files changed
+195 -23

No files matched your search

+63 -6
View File
@@ -204,12 +204,47 @@ function syncEmitters() {
emitterSync.sync(store, world, dotTex, trailTex, spineRuntime)
}
/** 事件互相连接的粒子系统共享最长暖场帧数,保证子发射器也能从父事件产生。 */
function warmupFramesBySystem() {
const byId = new Map(store.systems.map((system) => [system.id, system]))
const subEmitterIds = collectSubEmitterIds(store.systems)
const neighbors = new Map(store.systems.map((system) => [system.id, new Set<number>()]))
for (const system of store.systems) {
if (system.config.eventEnabled === false) continue
for (const rule of system.config.events || []) {
if (!byId.has(rule.targetSystemId) || rule.targetSystemId === system.id) continue
neighbors.get(system.id)!.add(rule.targetSystemId)
neighbors.get(rule.targetSystemId)!.add(system.id)
}
}
const result = new Map<number, number>()
for (const system of store.systems) {
if (result.has(system.id)) continue
const component: number[] = []
const pending = [system.id]
while (pending.length) {
const id = pending.pop()!
if (component.includes(id)) continue
component.push(id)
pending.push(...neighbors.get(id)!)
}
const frames = Math.max(0, ...component.map((id) => {
if (subEmitterIds.has(id)) return 0
const config = byId.get(id)?.config
if (config?.mode === 'stream' && config.streamBehavior === 'loop') return 0
return Math.floor(Number(config?.warmupFrames) || 0)
}))
for (const id of component) result.set(id, frames)
}
return result
}
/**
* 同步烘焙一个系统的整段帧缓存。
* 与逐帧录制结果一致(同样的固定步长、同样的随机序列),因此不会改变预览内容,
* 只是把"参数变动时就该生成好的烘焙结果"补齐。
*/
function bakeSystemFrames(system: ParticleSystem, emitter: ParticleEmitter, fps: number, totalFrames: number) {
function bakeSystemFrames(system: ParticleSystem, emitter: ParticleEmitter, fps: number, totalFrames: number, warmupFrames: number) {
const frameCount = Math.max(2, Math.floor(totalFrames) || 2)
// 循环持续本来就是一次性烘焙,直接复用同一套逻辑。
if (system.config.mode === 'stream' && system.config.streamBehavior === 'loop') {
@@ -218,10 +253,12 @@ function bakeSystemFrames(system: ParticleSystem, emitter: ParticleEmitter, fps:
}
emitter.reset()
const frameDuration = 1 / Math.max(1, fps)
emitter.warmup(warmupFrames, frameDuration)
const frames: ParticleState[][] = []
for (let frame = 0; frame < frameCount; frame++) {
// 与逐帧录制保持一致:第 0 帧保持空白,避免正式播放前提前发射。
if (frame === 0) { emitter.apply([]); frames.push([]) } else frames.push(emitter.update(frameDuration))
// 第 0 帧使用暖场后的状态;未设置暖场时仍保持从空白开始。
if (frame === 0) frames.push(warmupFrames > 0 ? emitter.capture() : [])
else frames.push(emitter.update(frameDuration))
}
system.frames = frames
}
@@ -233,6 +270,7 @@ 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)
const warmupFrames = warmupFramesBySystem()
// 烘焙预热会大量出生/死亡粒子,期间暂停跨系统事件,避免注入预热粒子。
emitterSync.suspendEvents(true)
try {
@@ -242,7 +280,7 @@ function ensureFramesBaked(fps: number) {
if ((system.frames?.length || 0) >= totalFrames) continue
const emitter = emitterSync.emitters.get(system.id)
if (!emitter) continue
bakeSystemFrames(system as unknown as ParticleSystem, emitter, fps, totalFrames)
bakeSystemFrames(system as unknown as ParticleSystem, emitter, fps, totalFrames, warmupFrames.get(system.id) || 0)
}
} finally {
emitterSync.suspendEvents(false)
@@ -398,6 +436,7 @@ function loop() {
displayFrame = tl.frame
collected.length = 0
total = 0
const warmedSystems = new Set<number>()
if (!tl.recorded) {
// 录制阶段:固定步长模拟并记录当前帧。
emitterSync.suspendEvents(true)
@@ -421,14 +460,32 @@ function loop() {
} finally {
emitterSync.suspendEvents(false)
}
if (tl.frame === 0) {
const warmupFrames = warmupFramesBySystem()
const targets = store.systems.flatMap((system) => {
const emitter = emitterSync.emitters.get(system.id)
const isLoopStream = system.config.mode === 'stream' && system.config.streamBehavior === 'loop'
const frames = warmupFrames.get(system.id) || 0
return emitter && !isLoopStream && frames > 0 ? [{ system, emitter, frames }] : []
})
const lastFrame = Math.max(0, ...targets.map(({ frames }) => frames))
for (let warmupFrame = 0; warmupFrame < lastFrame; warmupFrame++) {
for (const target of targets) {
if (warmupFrame < target.frames) target.emitter.update(frameDuration)
}
}
for (const { system } of targets) warmedSystems.add(system.id)
}
for (const [systemId, em] of emitterSync.emitters) {
const system = store.systems.find((item) => item.id === systemId)
const isLoopStream = system?.config.mode === 'stream' && system.config.streamBehavior === 'loop'
const loopFrame = isLoopStream
? system.frames?.[tl.frame]
: undefined
const forceEmptyInitialFrame = tl.frame === 0 && !isLoopStream
const states = forceEmptyInitialFrame ? [] : (loopFrame || em.update(frameDuration))
const forceEmptyInitialFrame = tl.frame === 0 && !isLoopStream && !warmedSystems.has(systemId)
const states = forceEmptyInitialFrame
? []
: loopFrame || (warmedSystems.has(systemId) ? em.capture() : em.update(frameDuration))
if (forceEmptyInitialFrame) em.apply([])
else if (loopFrame) em.apply(loopFrame)
if (system?.visible !== false) {