Files

129 lines
4.6 KiB
Go

package gpupreview
import (
"fmt"
"aego/art"
"aego/asset"
"aego/core/mathx"
"aego/modules/particles"
"aego/scene"
)
// configureParticles owns the preview's runtime emitter binding. The source
// document and the cached engine presets remain independent of presentation.
func (a *App) configureParticles(templateID, presetName string, preset particles.ParticleSystem2D, restart, clearRateOverride bool, spriteWidth float32) error {
p, o := a.data, a.options
refs := p.frames[a.status.Frame]
pc, _ := scene.Get[particles.ParticleSystem2D](a.sc, a.emitter)
changeTemplate := a.particleDocument != p.info.ID || a.particleTemplate != templateID
restart = restart || changeTemplate
if templateID != "" {
// Keep the real engine preset's burst schedule, curves, materials and
// lifecycle. The authored flipbook replaces only its image references.
// Apply defaults only for a new document/template, not each edited pixel
// or playback frame: host-controlled preview settings stay intact.
if changeTemplate {
frames := pc.Frames[:0]
*pc = preset
pc.Frames = frames
}
if o.RateOverride != nil {
pc.Rate = *o.RateOverride
} else if clearRateOverride {
pc.Rate = preset.Rate
}
a.particleExtent = particleFitExtent(*pc)
} else {
if a.particleTemplate != "" {
*pc = particles.DefaultParticleSystem2D()
}
pc.Material.ID, _ = asset.BuiltinMaterialNamed("Unlit")
pc.Rate = o.Rate
pc.Lifetime = mathx.V2(o.Lifetime, o.Lifetime)
pc.Speed = mathx.V2(o.Speed*.6, o.Speed)
pc.Size = mathx.V2(spriteWidth*.3, spriteWidth*.5)
pc.MaxParticles = 512
pc.Spread = mathx.Pi * 2
}
pc.Frames = pc.Frames[:0]
duration := 0
for _, fr := range p.info.Frames {
pc.Frames = append(pc.Frames, asset.Ref[asset.Texture]{ID: p.frames[fr.ID][art.Albedo]})
duration += fr.DurationMS
}
pc.Sprite.ID = refs[art.Albedo]
pc.FrameFPS = float32(len(p.info.Frames)) * 1000 / float32(max(duration, 1))
a.particleDocument, a.particleTemplate = p.info.ID, templateID
a.status.EffectTemplate, a.status.ParticlePreset = templateID, presetName
a.status.ParticleRate = pc.Rate
a.sc.Flush()
if restart && o.Mode == "particles" && !o.Paused {
return a.RestartParticles()
}
return nil
}
func (a *App) effectPreset(id string) (particles.ParticleSystem2D, string, error) {
template, ok := art.EffectTemplateOf(id)
if !ok {
return particles.ParticleSystem2D{}, "", fmt.Errorf("unknown effect template %q", id)
}
if preset, ok := a.presets[id]; ok {
return preset, template.ParticlePreset, nil
}
data, err := particles.PresetSource(template.ParticlePreset)
if err != nil {
return particles.ParticleSystem2D{}, "", err
}
preset, err := particles.ReadPreset(a.types, a.loader, data)
if err != nil {
return particles.ParticleSystem2D{}, "", err
}
a.presets[id] = preset
return preset, template.ParticlePreset, nil
}
// Fit world-space presets with the camera, leaving their simulation units,
// gravity and authored size curves unchanged.
func particleFitExtent(p particles.ParticleSystem2D) float32 {
radius := max(mathx.Abs(p.Offset.X), mathx.Abs(p.Offset.Y)) + p.Size.Y
switch p.Shape {
case particles.Circle:
radius += p.Radius
case particles.Rect:
radius += max(p.RectSize.X, p.RectSize.Y) * .5
case particles.Edge:
radius += max(mathx.Abs(p.EdgeA.X), mathx.Abs(p.EdgeA.Y), mathx.Abs(p.EdgeB.X), mathx.Abs(p.EdgeB.Y))
}
life := p.Lifetime.Y
radius += p.Speed.Y*life + .5*max(mathx.Abs(p.Gravity.X), mathx.Abs(p.Gravity.Y))*life*life
return max(1, radius*2.2)
}
// RestartParticles replays one-shot effects through the same CPU particle
// service used by a game. It does not edit or reset the source document.
func (a *App) RestartParticles() error {
if a.closed || a.data == nil || a.options.Mode != "particles" || a.options.Paused {
return fmt.Errorf("particle preview is not active")
}
p := particles.Of(a.world)
if p == nil {
return fmt.Errorf("particle preview service is unavailable")
}
// Service.target checks Scene.EffectiveEnabled before it asks for world
// transforms. Fresh nodes and a just-enabled emitter have not yet passed
// RenderPrepare, so Flush alone is insufficient and would drop this event.
// Resolve the private world's activation/pose without advancing simulation.
a.sc.Flush()
a.sc.UpdateTransforms()
p.Restart(a.emitter)
// Match the existing native particle preview transport: an event-driven
// Dust preset has neither a continuous rate nor authored burst keys. Replay
// supplies its explicit input event, without rewriting the preset itself.
if pc, ok := scene.Get[particles.ParticleSystem2D](a.sc, a.emitter); ok && !pc.PlayOnStart && pc.Rate == 0 && len(pc.Bursts) == 0 {
p.Burst(a.emitter, 8)
}
return nil
}