-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Texpacker #18
Add Texpacker #18
Changes from 11 commits
f37543e
25bf54b
b96044b
3b5b84a
c9ba390
faa1f33
933d1ff
85eb22b
9f73d3c
6fb5c53
8a39048
dd688bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ emulator | |
build | ||
.vscode | ||
docs/html | ||
*.swp |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
go get github.com/adinfinit/texpack/maxrect | ||
|
||
go build -o bin/objconv.exe ./objconv | ||
go build -o bin/bento.exe ./bento | ||
go build -o bin/gcpacker.exe ./gcpacker | ||
go build -o bin/texconv.exe ./texconv | ||
go build -o bin/tevasm.exe ./tevasm | ||
go build -o bin/tevasm.exe ./tevasm | ||
go build -o bin/texpacker.exe ./texpacker |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
go get github.com/adinfinit/texpack/maxrect | ||
|
||
go build -o bin/objconv ./objconv | ||
go build -o bin/bento ./bento | ||
go build -o bin/gcpacker ./gcpacker | ||
go build -o bin/texconv ./texconv | ||
go build -o bin/tevasm ./tevasm | ||
go build -o bin/tevasm ./tevasm | ||
go build -o bin/texpacker ./texpacker | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/binary" | ||
"fmt" | ||
"hash/fnv" | ||
) | ||
|
||
// 4 bytes | ||
type Vector2 struct { | ||
X, Y uint16 | ||
} | ||
|
||
// 8 bytes | ||
type Rect struct { | ||
Start, Size Vector2 | ||
} | ||
|
||
type FileHash uint32 | ||
|
||
// 4 + 4 = 8 bytes | ||
type Header struct { | ||
ParentTexture FileHash | ||
EntryCount int | ||
} | ||
|
||
// An Entry is a single texture's path + coordinates. 4 + 8 = 12 bytes | ||
type Entry struct { | ||
TexPath FileHash | ||
Coords Rect | ||
} | ||
|
||
var hash = fnv.New32() | ||
|
||
func ToFileHash(s string) FileHash { | ||
hash.Reset() | ||
_, err := fmt.Fprintf(hash, s) | ||
checkErr(err, "Failed to hash string %s", s) | ||
return FileHash(hash.Sum32()) | ||
} | ||
|
||
func (f FileHash) Bytes() []byte { | ||
out := make([]byte, 4) | ||
binary.BigEndian.PutUint32(out[0:], uint32(f)) | ||
return out | ||
} | ||
|
||
func (e Entry) Bytes() []byte { | ||
out := make([]byte, 12) | ||
binary.BigEndian.PutUint32(out[0:], uint32(e.TexPath)) | ||
binary.BigEndian.PutUint16(out[4:], e.Coords.Start.X) | ||
binary.BigEndian.PutUint16(out[6:], e.Coords.Start.Y) | ||
binary.BigEndian.PutUint16(out[8:], e.Coords.Size.X) | ||
binary.BigEndian.PutUint16(out[10:], e.Coords.Size.Y) | ||
return out | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"image" | ||
"os" | ||
"path/filepath" | ||
|
||
// Image formats | ||
_ "image/gif" | ||
_ "image/jpeg" | ||
_ "image/png" | ||
) | ||
|
||
func main() { | ||
flag.Usage = func() { | ||
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <file1> [<file2> ...]\n", os.Args[0]) | ||
flag.PrintDefaults() | ||
fmt.Fprint(os.Stderr, "\nSupported input formats:\n") | ||
for _, format := range []string{"JPEG", "GIF", "PNG"} { | ||
fmt.Fprintf(os.Stderr, " %s\n", format) | ||
} | ||
} | ||
outpath := flag.String("o", "out.png", "Output file") | ||
maxW := flag.Int("maxwidth", 1<<16, "Max width of output texture in pixels") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Up for discussion, do we wanna support non-square atlasses? if not, only needs 1 flag ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No reason to, I think. |
||
maxH := flag.Int("maxheight", 1<<16, "Max height of output texture in pixels") | ||
cwd, err := os.Getwd() | ||
checkErr(err, "Couldn't get working directory") | ||
stripPfx := flag.String("prefix", cwd, "Prefix to strip from file paths for hashing") | ||
flag.Parse() | ||
|
||
if flag.NArg() < 1 { | ||
fmt.Fprintf(os.Stderr, "[FAIL] No input files were specified\n") | ||
os.Exit(1) | ||
} | ||
|
||
// All positional arguments are input files | ||
inputFiles := flag.Args() | ||
|
||
maxBounds := image.Point{*maxW, *maxH} | ||
absoutpath, err := filepath.Abs(*outpath) | ||
checkErr(err, "Error converting outpath %s to absolute", *outpath) | ||
packer := NewTexPacker(absoutpath, TexPackerOptions{ | ||
MaxBounds: maxBounds, | ||
StripPrefix: *stripPfx, | ||
}) | ||
// Read images from input | ||
for _, path := range inputFiles { | ||
// Convert file path to absolute | ||
abspath, err := filepath.Abs(path) | ||
checkErr(err, "Error converting file path %s to absolute", path) | ||
packer.Add(abspath) | ||
} | ||
checkErr(packer.Save(), "Failed to save packed texture") | ||
} | ||
|
||
func checkErr(err error, msg string, args ...interface{}) { | ||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "[FAIL] "+msg+":\n ", args...) | ||
fmt.Fprintln(os.Stderr, err.Error()) | ||
os.Exit(1) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"image" | ||
"image/draw" | ||
"image/png" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/adinfinit/texpack/maxrect" | ||
) | ||
|
||
type TexPackerOptions struct { | ||
MaxBounds image.Point | ||
StripPrefix string | ||
} | ||
|
||
type TexPacker struct { | ||
outfile string | ||
options TexPackerOptions | ||
images []imageInfo | ||
} | ||
|
||
type imageInfo struct { | ||
Image image.Image | ||
Path string | ||
Coords Rect | ||
} | ||
|
||
func NewTexPacker(outfile string, options TexPackerOptions) *TexPacker { | ||
return &TexPacker{ | ||
outfile: outfile, | ||
options: options, | ||
} | ||
} | ||
|
||
// Add specifies a new texture to be packed. | ||
func (packer *TexPacker) Add(texpath string) error { | ||
input, err := os.Open(texpath) | ||
checkErr(err, "Cannot open input file: "+texpath) | ||
defer input.Close() | ||
|
||
img, fmt, err := image.Decode(input) | ||
if err != nil { | ||
return err | ||
} | ||
if fmt != "png" && fmt != "jpeg" && fmt != "gif" { | ||
return errors.New("Unsupported input format: " + fmt) | ||
} | ||
// Strip path prefix | ||
relpath, err := filepath.Rel(packer.options.StripPrefix, texpath) | ||
checkErr(err, "Error converting absolute path %s to relative (with base %s)", texpath, packer.options.StripPrefix) | ||
packer.images = append(packer.images, imageInfo{ | ||
Image: img, | ||
Path: relpath, | ||
}) | ||
return nil | ||
} | ||
|
||
// Save packs all the given textures into one and writes the result into `output` | ||
func (packer *TexPacker) Save() error { | ||
imageOut, headerOut, err := packer.getOutputs() | ||
if err != nil { | ||
return err | ||
} | ||
defer imageOut.Close() | ||
defer headerOut.Close() | ||
|
||
outtex, err := packer.pack() | ||
if err != nil { | ||
return err | ||
} | ||
if err = packer.writeHeader(headerOut); err != nil { | ||
return err | ||
} | ||
return png.Encode(imageOut, outtex) | ||
} | ||
|
||
// getOutput opens the output files and returns their handle | ||
func (packer *TexPacker) getOutputs() (imageOut io.WriteCloser, headerOut io.WriteCloser, err error) { | ||
// Get output writer | ||
imageOut, err = os.Create(packer.outfile) | ||
if err != nil { | ||
return | ||
} | ||
headerOut, err = os.Create(packer.outfile + ".atlas") | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this returns an error, |
||
} | ||
|
||
// pack runs the maxrect algorithms on the input textures, then writes the result | ||
// in the output texture, which is returned | ||
func (packer *TexPacker) pack() (image.Image, error) { | ||
|
||
points := getImageSizes(packer.images) | ||
|
||
outsize, bounds, ok := minimizeFit(packer.options.MaxBounds, points) | ||
if !ok { | ||
return nil, errors.New("Couldn't pack all images in given bounds!") | ||
} | ||
|
||
// Create and write packed texture | ||
outtex := image.NewRGBA(image.Rect(0, 0, outsize.X, outsize.Y)) | ||
for i, imginfo := range packer.images { | ||
srcbounds := bounds[i] | ||
r := image.Rectangle{srcbounds.Min, srcbounds.Min.Add(imginfo.Image.Bounds().Size())} | ||
|
||
// Copy whole imginfo.Image to rectangle `r` in outtex | ||
draw.Draw(outtex, r, imginfo.Image, image.Point{0, 0}, draw.Src) | ||
|
||
packer.images[i].Coords = Rect{ | ||
Start: Vector2{uint16(srcbounds.Min.X), uint16(srcbounds.Min.Y)}, | ||
Size: Vector2{uint16(srcbounds.Dx()), uint16(srcbounds.Dy())}, | ||
} | ||
|
||
// No need to keep this anymore | ||
packer.images[i].Image = nil | ||
} | ||
|
||
return outtex, nil | ||
} | ||
|
||
// writeHeader outputs binary metadata on the packed textures to the given Writer. | ||
func (packer *TexPacker) writeHeader(output io.Writer) error { | ||
|
||
nEntries := len(packer.images) | ||
|
||
// Output file format is: | ||
// ParentTexture Hash [4B] | ||
// Entry Count [4B] | ||
// Entry0 [12B] | ||
// ... | ||
|
||
// Used to check hash collisions | ||
hashes := make(map[FileHash]string, nEntries) | ||
|
||
// Parent Texture Hash | ||
relpath, err := filepath.Rel(packer.options.StripPrefix, packer.outfile) | ||
checkErr(err, "Error converting absolute path %s to relative (with base %s)", packer.outfile, packer.options.StripPrefix) | ||
ptHash := ToFileHash(relpath) | ||
hashes[ptHash] = relpath | ||
if _, err := output.Write(ptHash.Bytes()); err != nil { | ||
return err | ||
} | ||
|
||
// Entry Count | ||
countbuf := make([]byte, 4) | ||
binary.BigEndian.PutUint32(countbuf, uint32(nEntries)) | ||
if _, err := output.Write(countbuf); err != nil { | ||
return err | ||
} | ||
|
||
// Entries | ||
for _, imginfo := range packer.images { | ||
hash := ToFileHash(imginfo.Path) | ||
if orig, collides := hashes[hash]; collides { | ||
return fmt.Errorf("Hash conflict detected between the following files:\n [%8x] %s\n [%8x] %s", hash, orig, hash, imginfo.Path) | ||
} | ||
hashes[hash] = imginfo.Path | ||
entry := Entry{ | ||
TexPath: hash, | ||
Coords: imginfo.Coords, | ||
} | ||
if _, err := output.Write(entry.Bytes()); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func getImageSizes(images []imageInfo) []image.Point { | ||
points := make([]image.Point, len(images)) | ||
for i, imginfo := range images { | ||
points[i] = imginfo.Image.Bounds().Size() | ||
} | ||
return points | ||
} | ||
|
||
// Taken from https://github.com/adinfinit/texpack/blob/master/pack/fit.go | ||
func minimizeFit(maxContextSize image.Point, sizes []image.Point) (contextSize image.Point, rects []image.Rectangle, ok bool) { | ||
|
||
try := func(size image.Point) ([]image.Rectangle, bool) { | ||
context := maxrect.New(size) | ||
return context.Adds(sizes...) | ||
} | ||
|
||
contextSize = maxContextSize | ||
rects, ok = try(contextSize) | ||
if !ok { | ||
return | ||
} | ||
|
||
shrunk, shrinkX, shrinkY := true, true, true | ||
for shrunk { | ||
shrunk = false | ||
if shrinkX { | ||
trySize := image.Point{contextSize.X - 128, contextSize.Y} | ||
tryRects, tryOk := try(trySize) | ||
if tryOk { | ||
contextSize = trySize | ||
rects = tryRects | ||
shrunk = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will still cause it to try shrinkY, is that ok? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's probably a typo in the function I copy-pasted, it should be |
||
} else { | ||
shrinkX = false | ||
} | ||
} | ||
|
||
if shrinkY { | ||
trySize := image.Point{contextSize.X, contextSize.Y - 128} | ||
tryRects, tryOk := try(trySize) | ||
if tryOk { | ||
contextSize = trySize | ||
rects = tryRects | ||
shrunk = true | ||
} else { | ||
shrinkY = false | ||
} | ||
} | ||
} | ||
|
||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add the "go get" command on top of both build files, just like tools-extra/build.*