feat: add dithering param and parallel frame rendering for GIF
- Add dithering bool param to GIFRequest (default true) - Parallelize frame rendering with goroutines - Conditional Floyd-Steinberg dithering for speed vs quality tradeoff
This commit is contained in:
parent
0cbbe647f9
commit
fb3c4a0107
4 changed files with 29 additions and 12 deletions
|
|
@ -103,7 +103,7 @@ func (h *Handlers) HandleGIF(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gifBytes, err := render.RenderGIF(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.Delay)
|
gifBytes, err := render.RenderGIF(result.GLBBytes, result.Atlas, req.Background, req.Frames, req.Width, req.Height, req.Delay, *req.Dithering)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, "render failed: "+err.Error())
|
writeError(w, http.StatusInternalServerError, "render failed: "+err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,8 @@ const OpenAPISpec = `{
|
||||||
"frames": {"type": "integer", "default": 36, "description": "Number of frames (36 = 10° per frame)"},
|
"frames": {"type": "integer", "default": 36, "description": "Number of frames (36 = 10° per frame)"},
|
||||||
"width": {"type": "integer", "default": 512, "description": "Image width in pixels"},
|
"width": {"type": "integer", "default": 512, "description": "Image width in pixels"},
|
||||||
"height": {"type": "integer", "default": 512, "description": "Image height in pixels"},
|
"height": {"type": "integer", "default": 512, "description": "Image height in pixels"},
|
||||||
"delay": {"type": "integer", "default": 5, "description": "Centiseconds between frames"}
|
"delay": {"type": "integer", "default": 5, "description": "Centiseconds between frames"},
|
||||||
|
"dithering": {"type": "boolean", "default": true, "description": "Enable Floyd-Steinberg dithering (disable for faster rendering)"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ErrorResponse": {
|
"ErrorResponse": {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ type GIFRequest struct {
|
||||||
Width int `json:"width"` // default 512
|
Width int `json:"width"` // default 512
|
||||||
Height int `json:"height"` // default 512
|
Height int `json:"height"` // default 512
|
||||||
Delay int `json:"delay"` // centiseconds between frames, default 5
|
Delay int `json:"delay"` // centiseconds between frames, default 5
|
||||||
|
Dithering *bool `json:"dithering"` // Floyd-Steinberg dithering, default true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorResponse represents an error returned by the API
|
// ErrorResponse represents an error returned by the API
|
||||||
|
|
@ -56,4 +57,8 @@ func (r *GIFRequest) ApplyDefaults() {
|
||||||
if r.Background == "" {
|
if r.Background == "" {
|
||||||
r.Background = "#FFFFFF"
|
r.Background = "#FFFFFF"
|
||||||
}
|
}
|
||||||
|
if r.Dithering == nil {
|
||||||
|
defaultDithering := true
|
||||||
|
r.Dithering = &defaultDithering
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,13 @@ import (
|
||||||
"image/color/palette"
|
"image/color/palette"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/hytale-tools/blockymodel-merger/pkg/texture"
|
"github.com/hytale-tools/blockymodel-merger/pkg/texture"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderGIF renders a GLB model to an animated GIF rotating 360 degrees
|
// 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) ([]byte, error) {
|
func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames, width, height, delay int, dithering bool) ([]byte, error) {
|
||||||
// Parse background color
|
// Parse background color
|
||||||
bgColor, err := ParseHexColor(background)
|
bgColor, err := ParseHexColor(background)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -38,21 +39,31 @@ func RenderGIF(glbBytes []byte, atlas *texture.Atlas, background string, frames,
|
||||||
LoopCount: 0, // 0 = infinite loop
|
LoopCount: 0, // 0 = infinite loop
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render each frame
|
// Render frames in parallel
|
||||||
|
var wg sync.WaitGroup
|
||||||
for i := 0; i < frames; i++ {
|
for i := 0; i < frames; i++ {
|
||||||
rotation := float64(i) * rotationPerFrame
|
wg.Add(1)
|
||||||
|
go func(frameIdx int) {
|
||||||
|
defer wg.Done()
|
||||||
|
rotation := float64(frameIdx) * rotationPerFrame
|
||||||
|
|
||||||
// Render frame
|
// Render frame
|
||||||
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
|
img := RenderScene(mesh, atlasImage, rotation, width, height, bgColor)
|
||||||
|
|
||||||
// Quantize to palette
|
// Quantize to palette
|
||||||
paletted := image.NewPaletted(img.Bounds(), palette.Plan9)
|
paletted := image.NewPaletted(img.Bounds(), palette.Plan9)
|
||||||
|
if dithering {
|
||||||
draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{})
|
draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.Point{})
|
||||||
|
} else {
|
||||||
g.Image[i] = paletted
|
draw.Draw(paletted, img.Bounds(), img, image.Point{}, draw.Src)
|
||||||
g.Delay[i] = delay
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
g.Image[frameIdx] = paletted
|
||||||
|
g.Delay[frameIdx] = delay
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
// Encode GIF
|
// Encode GIF
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := gif.EncodeAll(&buf, g); err != nil {
|
if err := gif.EncodeAll(&buf, g); err != nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue