Skip to content

Instantly share code, notes, and snippets.

@cypherdare
Created December 29, 2015 20:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cypherdare/eefa5735a1537c188089 to your computer and use it in GitHub Desktop.
Save cypherdare/eefa5735a1537c188089 to your computer and use it in GitHub Desktop.
Libgdx texture packing with gifs
/*******************************************************************************
* Copyright 2015 Cypher Cove LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.cyphercove.dayinspace.desktop;
import com.badlogic.gdx.tools.*;
import com.badlogic.gdx.tools.texturepacker.*;
import com.cyphercove.dayinspace.*;
import org.apache.commons.io.*;
import org.apache.commons.io.filefilter.*;
import java.io.*;
import java.util.*;
public class AtlasGenerator {
static final String SOURCE_DIR = "planning/Texture Atlas";
static final String TARGET_DIR = "core/assets";
public static void main (String[] args) throws Exception {
//Delete old pack
File oldPackFile = new File(TARGET_DIR + "/" + Assets.MAIN_ATLAS + Assets.ATLAS_EXTENSION);
if (oldPackFile.exists()){
System.out.println("Deleting old pack file");
oldPackFile.delete();
}
//Delete old font files
Collection<File> oldFontFiles = FileUtils.listFiles(
new File(TARGET_DIR),
new RegexFileFilter(".*\\.fnt"),
TrueFileFilter.INSTANCE
);
for (File file : oldFontFiles){
System.out.println("Copying font file: " + file.getName());
FileUtils.deleteQuietly(file);
}
//Create PNGs for GIF frames
GifProcessor gifProcessor = new GifProcessor(0.015f);
ArrayList<FileProcessor.Entry> gifFrames = gifProcessor.process(SOURCE_DIR, SOURCE_DIR);
//Pack them
TexturePacker.Settings settings = new TexturePacker.Settings();
settings.atlasExtension = Assets.ATLAS_EXTENSION;
TexturePacker.process(
settings,
SOURCE_DIR,
TARGET_DIR,
Assets.MAIN_ATLAS);
//Copy over any fonts
Collection<File> fontFiles = FileUtils.listFiles(
new File(SOURCE_DIR),
new RegexFileFilter(".*\\.fnt"),
TrueFileFilter.INSTANCE
);
File destDir = new File(TARGET_DIR);
for (File file : fontFiles){
System.out.println("Copying font file: " + file.getName());
FileUtils.copyFileToDirectory(file, destDir);
}
//Delete the GIF frames that were generated.
for (File file : gifProcessor.getGeneratedFiles())
file.delete();
}
}
/*******************************************************************************
* Copyright 2015 Cypher Cove LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.cyphercove.dayinspace.desktop;
import java.util.*;
/**
* Calculates an approximate greatest common divisor for floating point numbers, given a maximum allowable error.
* <p> Based on algorithm described <a href="http://stackoverflow.com/a/479028/506796">here</a>.
*/
public class FloatingPointGCD {
public static float findFloatingPointGCD(Float[] input, float maxError) {
//Sanitize input
Set<Float> inputSet = new HashSet<Float>(input.length);
for (Float f : input) {
if (f!=0) inputSet.add(Math.abs(f));
}
if (inputSet.size() <= 1)
return Math.abs(input[0]);
float lowestValue = input[0];
float highestValue = lowestValue;
for (int i=1; i<input.length; i++){
float value = input[i];
if (value<lowestValue) lowestValue = value;
if (value>highestValue) highestValue = value;
}
//Find fast worst case GCD to limit upcoming rational fraction tree search
int[] errorScaledInput = new int[input.length];
for (int i = 0; i < input.length; i++) {
errorScaledInput[i] = Math.round(input[i] / maxError);
}
float worstCaseGCD = gcd(errorScaledInput) * maxError;
//Express input set as ratios to lowest value. If these ratios can be expressed as
//rational fractions that all have the same denominator, then that denominator represents
//a potential solution when the lowest value is divided by it.
inputSet.remove(lowestValue);
List<Float> scaledInput = new ArrayList<Float>(inputSet.size());
for (Float f : inputSet)
scaledInput.add(f/lowestValue);
//Find set of all possible fractions that might produce better result than worstCaseGCD
int maxNumerator = Math.round(highestValue / worstCaseGCD);
int maxDenominator = Math.round(lowestValue / worstCaseGCD);
Set<Rational> tree = generateSternBrocotTree(maxNumerator, maxDenominator);
//Find subset of rational representations of each member of input set that don't violate
//max error.
List<Set<Rational>> validRationals = new ArrayList<Set<Rational>>();
for (Float f : scaledInput){
float unscaledF = f*lowestValue;
Set<Rational> valids = new HashSet<Rational>();
for (Rational rat : tree){
float gcd = lowestValue/rat.den;
float error = Math.abs(rat.num*gcd - unscaledF);
if (error <= maxError)
valids.add(rat);
}
validRationals.add(valids);
}
//Find all denominators that are shared among all solutions sets. Score each solution
//by summing all the numerators and the denominator.
int bestSolutionSum = -1;
int bestSolution = -1;
for (int den = 1; den < maxDenominator+1; den++) {
boolean denIsValidForAll = true;
int solutionSum = den;
for (Set<Rational> floatsSet : validRationals){
boolean denIsValidForFloat = false;
for (Rational rat : floatsSet){
if (rat.den == den) {
solutionSum += rat.den;
denIsValidForFloat = true;
break;
}
}
if (!denIsValidForFloat){
denIsValidForAll = false;
break;
}
}
if (denIsValidForAll && (bestSolutionSum <0 || solutionSum<bestSolutionSum)){
bestSolutionSum = solutionSum;
bestSolution = den;
}
}
float gcd = bestSolution > 0 ? lowestValue/bestSolution : worstCaseGCD;
//Minimize error by calculating linear regression
inputSet.add(lowestValue); //was removed earlier
Map<Float, Float> points = new HashMap<Float, Float>(inputSet.size());
for (Float f : inputSet){
points.put(f, (float)Math.round(f/gcd));
}
gcd = calculateRegressionSlope(points);
return gcd;
}
private static class Rational {
public int num, den;
public Rational(int num, int den){
this.num = num; this.den = den;
}
public static Rational mediant(Rational a, Rational b){
return new Rational(a.num + b.num, a.den + b.den);
}
public boolean equals(Object obj){
if (obj instanceof Rational){
Rational rat = (Rational)obj;
return rat.num==num && rat.den==den;
}
return false;
}
public String toString(){
return num + ":" + den;
}
}
private static Set<Rational> generateSternBrocotTree(int maxNumerator, int maxDenominator){
Set<Rational> tree = new HashSet<Rational>();
Rational left = new Rational(0,1);
Rational right = new Rational(1,0);
addSternBrocotBranch(tree, left, right, maxNumerator, maxDenominator);
return tree;
}
private static void addSternBrocotBranch(Set<Rational> tree, Rational left, Rational right, int maxNumerator, int maxDenominator){
Rational mediant = Rational.mediant(left, right);
if (mediant.num > maxNumerator || mediant.den > maxDenominator)
return;
tree.add(mediant);
addSternBrocotBranch(tree, left, mediant, maxNumerator, maxDenominator); //add left branch
addSternBrocotBranch(tree, mediant, right, maxNumerator, maxDenominator); //add right branch
}
private static int gcd(int[] in){
if (in.length==1)
return in[0];
int gcd = in[0];
for (int i=1; i<in.length; i++){
gcd = gcd(gcd, in[i]);
}
return gcd;
}
private static int gcd(int a, int b){
if(a == 0 || b == 0) return a+b;
return gcd(b,a%b);
}
private static float calculateRegressionSlope(Map<Float, Float> points){
float meanX = 0, meanY = 0;
int numPoints = points.size();
for(Map.Entry<Float, Float> entry : points.entrySet()){
meanX += entry.getKey();
meanY += entry.getValue();
}
meanX /= numPoints;
meanY /= numPoints;
float stDevX = 0, stDevY = 0, r = 0;
for(Map.Entry<Float, Float> entry : points.entrySet()){
float xDiff = entry.getKey()-meanX;
float yDiff = entry.getValue()-meanY;
stDevX += xDiff*xDiff;
stDevY += yDiff*yDiff;
r += xDiff*yDiff;
}
r /= (float)Math.sqrt(stDevX*stDevY);
stDevX = (float)Math.sqrt(stDevX/numPoints);
stDevY = (float)Math.sqrt(stDevY/numPoints);
return r * stDevX / stDevY;
}
}
/*******************************************************************************
* Copyright 2015 Cypher Cove LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.cyphercove.dayinspace.desktop;
import com.badlogic.gdx.tools.*;
import javax.imageio.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
/**
* Take all gif files along with potential .alpha counterparts, and write png images
* to destination in like directories.
*/
public class GifProcessor extends FileProcessor {
private static final float GIF_DELAYTIME_TO_SECONDS = 0.01f;
private static final Color TRANSPARENT = new Color(0x00000000, true);
private float maxAnimationDelayError;
private ArrayList<File> generatedFiles = new ArrayList<File>();
public GifProcessor (float maxAnimationDelayError){
this.maxAnimationDelayError = maxAnimationDelayError;
addInputSuffix(".gif");
setInputFilter(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !name.toLowerCase().endsWith(".alpha.gif");
}
});
}
@Override
public ArrayList<Entry> process (File inputFile, File outputRoot) throws Exception {
generatedFiles.clear();
return super.process(inputFile, outputRoot);
}
@Override
protected void processFile (Entry entry) throws Exception {
File file = entry.inputFile;
String fileName = file.getName();
int dotIndex = fileName.lastIndexOf('.');
String name = (dotIndex != -1) ? fileName.substring(0, dotIndex) : fileName;
String alphaFileName = name + ".alpha.gif";
File alphaFile = new File(file.getParent(), alphaFileName);
if (!alphaFile.exists()){
alphaFileName = name + ".alpha.GIF";
alphaFile = new File(file.getParent(), alphaFileName);
}
LinkedHashMap<BufferedImage, Float> images = getGifImages(file, true);
ArrayList<BufferedImage> alphaImages = null;
if (alphaFile.exists()){
LinkedHashMap<BufferedImage, Float> alphaImagesMap = getGifImages(alphaFile, false);
alphaImages = new ArrayList<BufferedImage>(alphaImagesMap.size());
for (BufferedImage image : alphaImagesMap.keySet()){
alphaImages.add(image);
}
}
boolean oneFrame = images.size()==1;
float delay = 1;
if (!oneFrame) {
Float[] delays = new Float[images.size()];
delay = FloatingPointGCD.findFloatingPointGCD(
images.values().toArray(delays), maxAnimationDelayError);
System.out.println("Animation \"" + name + "\" uses delay of " + delay);
}
int index = -1;
int imageNum = 0;
for (Map.Entry<BufferedImage, Float> imageEntry : images.entrySet()){
BufferedImage image = imageEntry.getKey();
if (alphaImages!=null){
applyGrayscaleMaskToAlpha(image, alphaImages.get(imageNum));
}
int count = oneFrame ? 1 : Math.round(imageEntry.getValue() / delay);
for (int i = 0; i < count; i++) {
index++;
if (!entry.outputDir.exists()) entry.outputDir.mkdir();
File outputFile = new File(entry.outputDir, oneFrame? name + ".png" : name + "_" + index + ".png");
ImageIO.write(image, "png", outputFile);
generatedFiles.add(outputFile);
}
imageNum++;
}
}
public ArrayList<File> getGeneratedFiles() {
return generatedFiles;
}
/** From http://stackoverflow.com/a/8058442/506796 */
public void applyGrayscaleMaskToAlpha(BufferedImage image, BufferedImage mask)
{
int width = image.getWidth();
int height = image.getHeight();
int[] imagePixels = image.getRGB(0, 0, width, height, null, 0, width);
int[] maskPixels = mask.getRGB(0, 0, width, height, null, 0, width);
for (int i = 0; i < imagePixels.length; i++)
{
int color = imagePixels[i] & 0x00ffffff; // Mask preexisting alpha
int alpha = maskPixels[i] << 24; // Shift green to alpha
imagePixels[i] = color | alpha;
}
image.setRGB(0, 0, width, height, imagePixels, 0, width);
}
private LinkedHashMap<BufferedImage, Float> getGifImages (File file, boolean withDelayTimes){
ImageReader imageReader = null;
LinkedHashMap<BufferedImage, Float> images = new LinkedHashMap<BufferedImage, Float>();
try {
ImageInputStream inputStream = ImageIO.createImageInputStream(file);
imageReader = ImageIO.getImageReaders(inputStream).next();
imageReader.setInput(inputStream);
int numImages = imageReader.getNumImages(true);
Image generalImage = ImageIO.read(file);
int width = generalImage.getWidth(null);
int height = generalImage.getHeight(null);
//start with blank image of the complete size, in case first frame isn't full size (possible?)
BufferedImage previousImage =
new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
for (int i = 0; i < numImages; i++) {
IIOMetadata imageMetaData = imageReader.getImageMetadata(i);
String metaFormatName = imageMetaData.getNativeMetadataFormatName();
IIOMetadataNode root = (IIOMetadataNode) imageMetaData.getAsTree(metaFormatName);
IIOMetadataNode imageDescriptorNode = getMetaDataNode(root, "ImageDescriptor");
int top = Integer.parseInt(imageDescriptorNode.getAttribute("imageTopPosition"));
int left = Integer.parseInt(imageDescriptorNode.getAttribute("imageLeftPosition"));
BufferedImage image = imageReader.read(i);
BufferedImage combinedImage =
new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D graphics = combinedImage.createGraphics();
graphics.drawImage(previousImage, 0, 0, null);
graphics.setBackground(TRANSPARENT);
graphics.clearRect(left, top, image.getWidth(), image.getHeight());
combinedImage.getGraphics().drawImage(image, left, top, null);
float frameDelay = 0;
if (withDelayTimes){
IIOMetadataNode graphicsControlExtensionNode = getMetaDataNode(root, "GraphicControlExtension");
frameDelay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime"))
* GIF_DELAYTIME_TO_SECONDS;
}
images.put(combinedImage, frameDelay);
previousImage = combinedImage;
}
} catch (IOException ex) {
throw new RuntimeException("Error reading image: " + file, ex);
} finally {
if (imageReader != null)
imageReader.setInput(null);
}
return images;
}
private static IIOMetadataNode getMetaDataNode(IIOMetadataNode rootNode, String nodeName) {
int nNodes = rootNode.getLength();
for (int i = 0; i < nNodes; i++) {
if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName)== 0) {
return((IIOMetadataNode) rootNode.item(i));
}
}
IIOMetadataNode node = new IIOMetadataNode(nodeName);
rootNode.appendChild(node);
return(node);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment