Generate optimal 256-color palette from rendered frames when dithering disabled, replacing generic Plan9 palette for better color accuracy without dithering artifacts.
89 lines
2.1 KiB
Go
89 lines
2.1 KiB
Go
package render
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/color/palette"
|
|
"image/draw"
|
|
"image/gif"
|
|
"sync"
|
|
|
|
"github.com/hytale-tools/blockymodel-merger/pkg/texture"
|
|
)
|
|
|
|
// RenderGIF renders a GLB model to an animated GIF rotating 360 degrees
|
|
func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int, dithering bool) ([]byte, error) {
|
|
// Parse background color
|
|
bgColor, err := ParseHexColor(background)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid background color: %w", err)
|
|
}
|
|
|
|
// Get atlas image
|
|
var atlasImage = atlas.Image
|
|
|
|
// Convert GLB to mesh
|
|
mesh, err := GLBToMesh(glbBytes, atlasImage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting GLB to mesh: %w", err)
|
|
}
|
|
|
|
// Calculate rotation per frame
|
|
rotationPerFrame := 360.0 / float64(frames)
|
|
|
|
// Render all frames first (in parallel)
|
|
renderedFrames := make([]image.Image, frames)
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < frames; i++ {
|
|
wg.Add(1)
|
|
go func(frameIdx int) {
|
|
defer wg.Done()
|
|
rotation := float64(frameIdx) * rotationPerFrame
|
|
renderedFrames[frameIdx] = RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Determine palette
|
|
var pal color.Palette
|
|
if dithering {
|
|
pal = palette.Plan9
|
|
} else {
|
|
pal = MedianCutQuantize(renderedFrames, 256)
|
|
}
|
|
|
|
// Create GIF structure
|
|
g := &gif.GIF{
|
|
Image: make([]*image.Paletted, frames),
|
|
Delay: make([]int, frames),
|
|
LoopCount: 0, // 0 = infinite loop
|
|
}
|
|
|
|
// Quantize frames to palette (in parallel)
|
|
for i := 0; i < frames; i++ {
|
|
wg.Add(1)
|
|
go func(frameIdx int) {
|
|
defer wg.Done()
|
|
img := renderedFrames[frameIdx]
|
|
paletted := image.NewPaletted(img.Bounds(), pal)
|
|
if dithering {
|
|
draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{})
|
|
} else {
|
|
draw.Draw(paletted, img.Bounds(), img, image.Point{}, draw.Src)
|
|
}
|
|
g.Image[frameIdx] = paletted
|
|
g.Delay[frameIdx] = delay
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
// Encode GIF
|
|
var buf bytes.Buffer
|
|
if err := gif.EncodeAll(&buf, g); err != nil {
|
|
return nil, fmt.Errorf("encoding GIF: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|