133 lines
3.2 KiB
Go
133 lines
3.2 KiB
Go
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
|
|
}
|