package main import ( "image" "log" "sort" "github.com/hajimehoshi/ebiten/v2" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" ) const ( // 2px Padding verhindert Linear-Filter-Bleeding an Sprite-Grenzen atlasPadding = 2 // Maximale Atlas-Größe; 2048 deckt alle aktuellen Sprites locker ab atlasMaxSize = 2048 ) // buildAtlas packt alle Sprite-Bilder in eine einzige GPU-Textur (Texture Atlas). // Ebiten erkennt zusammenhängende DrawImage-Calls auf derselben Quell-Textur // und fasst sie in einen einzigen GPU-Draw-Call zusammen ("internal batching"). // // Bilder vom Typ "background" und Bilder die größer als atlasMaxSize/2 sind // bleiben als eigenständige Texturen und werden direkt in das Ergebnis übernommen. // // Alle anderen Sprites werden per Shelf-Packing-Algorithmus in den Atlas gepackt. // Der Rückgabewert ist eine neue Map, in der jeder Eintrag entweder ein // atlas.SubImage()-Ausschnitt oder das originale Einzelbild ist. func buildAtlas(images map[string]*ebiten.Image, manifest game.AssetManifest) map[string]*ebiten.Image { result := make(map[string]*ebiten.Image, len(images)) type spriteEntry struct { id string w int h int } var topack []spriteEntry standalone := make(map[string]*ebiten.Image) for id, img := range images { if img == nil { continue } def, ok := manifest.Assets[id] if ok && def.Type == "background" { standalone[id] = img continue } w, h := img.Bounds().Dx(), img.Bounds().Dy() if w > atlasMaxSize/2 || h > atlasMaxSize/2 { standalone[id] = img continue } topack = append(topack, spriteEntry{id: id, w: w, h: h}) } if len(topack) == 0 { for id, img := range standalone { result[id] = img } return result } // Shelf-Packing: nach Höhe absteigend sortieren → dichtere Packung sort.Slice(topack, func(i, j int) bool { return topack[i].h > topack[j].h }) type placement struct { id string rect image.Rectangle } var placements []placement curX := atlasPadding curY := atlasPadding shelfH := 0 atlasW := 0 atlasH := 0 for _, s := range topack { pw := s.w + atlasPadding ph := s.h + atlasPadding // Neue Zeile (Shelf) wenn kein Platz mehr in aktueller Zeile if curX+pw > atlasMaxSize { curX = atlasPadding curY += shelfH + atlasPadding shelfH = 0 } // Passt nicht mehr in den Atlas → als Standalone behalten if curY+ph > atlasMaxSize { standalone[s.id] = images[s.id] continue } r := image.Rect(curX, curY, curX+s.w, curY+s.h) placements = append(placements, placement{id: s.id, rect: r}) if s.h > shelfH { shelfH = s.h } if curX+pw > atlasW { atlasW = curX + pw } if curY+s.h+atlasPadding > atlasH { atlasH = curY + s.h + atlasPadding } curX += pw } // Atlas-Textur anlegen und Sprites hineinzeichnen atlas := ebiten.NewImage(atlasW, atlasH) for _, p := range placements { op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(p.rect.Min.X), float64(p.rect.Min.Y)) atlas.DrawImage(images[p.id], op) result[p.id] = atlas.SubImage(p.rect).(*ebiten.Image) } for id, img := range standalone { result[id] = img } log.Printf("🗺️ Texture Atlas: %dx%d px | %d Sprites gepackt | %d standalone", atlasW, atlasH, len(placements), len(standalone)) return result }