Created
September 14, 2011 21:54
-
-
Save jamesu/1217908 to your computer and use it in GitHub Desktop.
Atlas generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Author: Matthias Hoechsmann, gamedrs.com | |
# Ported to Ruby by James Urquhart, github.com/jamesu | |
# Artefact Removal using Edge Copy by Patrick Wolowicz, subzero.eu | |
# This source code an be used freely and is provided "AS IS" without any warranties. | |
# Go and visit www.zombiesmash.gamedrs.com. Thank you :) | |
# README | |
# mkatlas.rb is a ruby script for generating an atlas image from individual images and a C header file with image coordinates. | |
# mkAtlas.rb takes a *.atl file in the following format and turns it into a png/pvr atlas. | |
# Format: each line is <imagename>[,+<scale factor img>][,-<scale factor code],[*<alternative variable name], square brackets are optional | |
# Here is an example and elaboration: | |
# | |
# File foreground.atl has the following content | |
# interface/houseEnergy/Housebar_barcolor_01.png,+1.5 | |
# emptyslot.png,+2,-0.5 | |
# interface/houseEnergy/Housebar_houseonly_concept01.png,*housebar_concept | |
# | |
# mkatlas.rb will pack all these images in one Atlas image foregroundAtlas.png. It will also create the pvr and pvr_png preview, | |
# named foregroundAtlas.pvr and foregroundAtlas_pvrprev.png. mkatlas.rb also create header files with coordinates that can be used | |
# for atlas sprites. In particular, it creates a file foregroundCoords.h that could look like | |
# | |
# #define COORDS_FOREGROUND_HOUSEBAR_BARCOLOR_01 Rect(1,19,1,7) | |
# #define SCALE_FOREGROUND_HOUSEBAR_BARCOLOR_01 1 | |
# #define COORDS_FOREGROUND_EMPTYSLOT Rect(38,33,44.8,44.8) | |
# #define SCALE_FOREGROUND_EMPTYSLOT 0.5 | |
# #define COORDS_FOREGROUND_HOUSEBAR_CONCEPT Rect(309,1,28,28) | |
# #define SCALE_FOREGROUND_HOUSEBAR_CONCEPT 1 | |
# | |
# For each line in the *.atl file, a rectangle and a scale factor is defined in the header file | |
# As default, the scale factor is 1 and can be ignored. Sometimes though you want to scale a sprite in your code. | |
# In particular, to improve pvr sprites you could scale the image up when copying it to the atlas and scale a sprite that uses the | |
# image down by the same proportion. This often gives better results than just using the pvr image 1:1. | |
# ",+1.5" scales Housebar_barcolor_01.png by factor 1.5 when copying it to the atlas. Thus, the rectangle will be 1.5 times as big as the | |
# original image. | |
# ",+2,-0.5" scales the original image by factor 2. It also generates a SCALE define set to 0.5. | |
# ",*housebar_concept" sets the define name. Otherwise, its deduced from the filename. | |
# | |
# For generating the atlas, call mkatlas like: | |
# ./mkatlas.rb -p 2 -s 512 -f foreground/foreground.atl -r .. | |
# -p sets the spacing for images. Here 2 pixels. When you use pvr, you want to increase the spacing to reduce artefacts | |
# -s 512 sets the atlas image size to 512 pixels. The number must be a power of 2. | |
# -f sets the atl file with lines as described above | |
# -r sets the root directory for images. All paths from the atl file are relative to this directory | |
# | |
# Happy coding! - Matthias | |
# Changes | |
# 10-08-02 added support for improved artefact removal with -p > 2 (required for mip mapping) | |
# 09-13-09 improved artefact removal when called with -p 2, removed warning about init not initialized | |
# 08-16-09 fixed bug: alpha channel not copied for first image in atl file | |
# 09-14-11 Converted to ruby by jamesu | |
require 'rubygems' | |
require 'rmagick' | |
require 'optparse' | |
$opts = {} | |
# change the path if texturetool is installed somewhere else | |
$texturetool = "/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/texturetool" | |
# packing algorithm based on http://www.blackpawn.com/texts/lightmaps/default.html | |
def insertImg(node, img) | |
# puts "w: #{node[:w]}, h: #{node[:h]}" | |
# if we're not a leaf then | |
if node[:childa] != 0 or node[:childb] != 0 | |
# (try inserting into first child) | |
newNode = insertImg(node[:childa], img) | |
return newNode if newNode != 0 | |
# (no room, insert into second) | |
newNode = insertImg(node[:childb], img) | |
return newNode | |
else | |
# (if there's already a texture here, return) | |
return 0 if node[:img_ref] != 0 | |
# (if we're too small, return) | |
return 0 if img[:w] > node[:w] or img[:h] > node[:h] | |
# (if we're just right, accept) | |
if node[:w] == img[:w] && node[:h] == img[:h] | |
node[:img_ref] = img | |
return node | |
end | |
# (otherwise, gotta split this node and create some kids) | |
childa = {:img_ref => 0, :childa => 0, :childb => 0} | |
childb = {:img_ref => 0, :childa => 0, :childb => 0} | |
# (decide which way to split) | |
dw = node[:w] - img[:w] | |
dh = node[:h] - img[:h] | |
if dw > dh | |
# split vertically | |
childa[:left] = node[:left] | |
childa[:bottom] = node[:bottom] | |
childa[:w] = img[:w] | |
childa[:h] = node[:h] | |
childb[:left] = node[:left] + img[:w] | |
childb[:bottom] = node[:bottom] | |
childb[:w] = node[:w] - img[:w] | |
childb[:h] = node[:h] | |
else | |
# split horizontally | |
childa[:left] = node[:left] | |
childa[:bottom] = node[:bottom] | |
childa[:w] = node[:w] | |
childa[:h] = img[:h] | |
childb[:left] = node[:left] | |
childb[:bottom] = node[:bottom] + img[:h] | |
childb[:w] = node[:w] | |
childb[:h] = node[:h] - img[:h] | |
end | |
node[:childa] = childa | |
node[:childb] = childb | |
# (insert into first child we created) | |
newNode = insertImg(node[:childa], img) | |
newNode | |
end | |
end | |
def main | |
num_images = 0 | |
images = [] | |
# read config file | |
in_file = File.open($opts[:f]) rescue die("Cannot open file: #{$opts[:f]}\n") | |
in_file.each_line do |line| | |
# skip comment lines (//) | |
if(line =~ /^\/\// or line =~ /^\s*$/) | |
next | |
else | |
scaleUp = 1 | |
scaleDown = 1 | |
altName = "" | |
# open image file and get stats | |
if line =~ /^(.*?),(.*)$/ | |
filename = "#{$opts[:r]}/#{$1}" | |
rest = $2 | |
scaleUp = $1 if rest =~ /\+(\d+\.?\d*)/ | |
scaleDown = $1 if rest =~ /-(\d+\.?\d*)/ | |
altName = $1 if rest =~ /\*(.\w*)/ | |
else | |
filename = "#{$opts[:r]}/#{line.strip}" | |
end | |
gd_image = Magick::Image.read(File.expand_path(filename))[0] #rescue die("Error: Cannot open image file #{filename}") | |
width, height = gd_image.columns, gd_image.rows | |
image = { | |
:filename => filename, | |
:w => (width*scaleUp)+$opts[:p], | |
:h => (height*scaleUp)+$opts[:p], | |
:scaleup => scaleUp, | |
:scaledown => scaleDown, | |
:altname => altName, | |
:w_org => width, | |
:h_org => height | |
} | |
images << image | |
puts "#{filename} -> w: #{width}, h: #{height}" | |
end | |
end | |
in_file.close | |
# build coordinate tree | |
atlasParam = {:w => $opts[:s], :h => $opts[:s]} | |
node = {:left => 0, :bottom => 0, :w => atlasParam[:w], :h => atlasParam[:h], :img_ref => 0, :childa => 0, :childb => 0} | |
images.each do |image| | |
res = insertImg(node, image) | |
puts "INSERT #{image[:filename]}" | |
if res == 0 | |
puts("Error: Image '#{image[:filename]}' could not be inserted, increase Atlas size") | |
break | |
end | |
end | |
# generate Atlas image and header | |
file_prefix = "" | |
file_prefix_short = "" | |
file_prefix = $1 if $opts[:f] =~ /^(.*)\.atl$/ | |
file_prefix = $opts[:f] if file_prefix == "" | |
file_prefix_short = file_prefix | |
file_prefix_short = file_prefix_short.gsub(/.*\/(.*)/, $1) | |
atlasParam[:prefix] = file_prefix_short.upcase | |
puts "# new coordinates" | |
gd_atlas = Magick::Image.new(atlasParam[:w], atlasParam[:h]) | |
gd_atlas.format = 'PNG' | |
#$gd_atlas->saveAlpha(1) | |
#$gd_atlas->alphaBlending(0) | |
atlasParam[:atlas_ref] = gd_atlas | |
File.open("#{file_prefix}Coords.h", 'w') do |h| | |
atlasParam[:header_fh] = h | |
h.write "// This file was automatically created by mkatlas.rb\n\n" | |
generateAtlas(node, atlasParam) | |
out = File.open("#{file_prefix}Atlas.png", 'w') rescue die("Cannot open atlas.png") | |
out.write gd_atlas.to_blob | |
out.close | |
end | |
# create pvr texture file | |
call = "#{$texturetool} -o \"#{file_prefix}Atlas.pvr\" -f PVR -e PVRTC -p \"#{file_prefix}Atlas_pvrprev.png\" \"#{file_prefix}Atlas.png\"" | |
puts "Calling #{call}" | |
`#{call}` | |
end | |
def generateAtlas(node, atlasParam) | |
if node[:img_ref] != 0 | |
img = node[:img_ref] | |
puts "#{img[:filename]}: #{node[:left]}, #{node[:bottom]}, #{node[:w]}, #{node[:h]}" | |
# GD::Image->trueColor(1) | |
gd_image_src = Magick::Image.read(node[:img_ref][:filename])[0] | |
#$gd_image_src->saveAlpha(1) | |
#$gd_image_src->alphaBlending(0) | |
#$gd_image_src->trueColor(1) | |
margin = $opts[:p]/2 | |
x_dest = node[:left] | |
y_dest = node[:bottom] | |
w_src = node[:img_ref][:w_org] | |
h_src = node[:img_ref][:h_org] | |
scaleUp = node[:img_ref][:scaleup] | |
scaleDown = node[:img_ref][:scaledown] | |
w_dst = w_src*scaleUp | |
h_dst = h_src*scaleUp | |
altName = node[:img_ref][:altname] | |
#copy image to atlas | |
if scaleUp == 1 | |
atlasParam[:atlas_ref].composite! gd_image_src, x_dest+margin, y_dest+margin, Magick::CopyCompositeOp | |
else | |
gd_image_src.scale w_dst, h_dst | |
atlasParam[:atlas_ref].composite! gd_image_src, x_dest+margin, y_dest+margin, Magick::CopyCompositeOp | |
end | |
# TODO | |
# fill surrounding margin with image | |
#if margin != 0 | |
# margin.times do |counter| | |
# atlasParam[:atlas_ref]->copy(atlasParam[:atlas_ref],$x_dest+$margin, $y_dest+$counter,$x_dest+$margin,$y_dest+$margin,$w_dst,1) #upper | |
# atlasParam[:atlas_ref]->copy(atlasParam[:atlas_ref],$x_dest+$margin, $y_dest+($margin*2)+$h_dst-$counter-1,$x_dest+$margin,$y_dest+$h_dst+$margin-1,$w_dst,1) #lower | |
# end | |
# margin.times do |counter| | |
# atlasParam[:atlas_ref]->copy(atlasParam[:atlas_ref],$x_dest+$counter,$y_dest,$x_dest+$margin,$y_dest,1,$h_dst+$margin*2) #left | |
# atlasParam[:atlas_ref]->copy(atlasParam[:atlas_ref],$x_dest+$margin+$w_dst+$counter,$y_dest,$x_dest+$w_dst+$margin-1,$y_dest,1,$h_dst+$margin*2) #right | |
# end | |
#end | |
#generate header line | |
if altName == "" | |
name = $1.upcase if node[:img_ref][:filename] =~ /^(.*)\..*$/ | |
name = $1 if name =~ /^.*\/(.*)$/ | |
else | |
name = altName.upcase | |
end | |
coordsName = "COORDS_#{atlasParam[:prefix]}_#{name}" | |
scaleName = "SCALE_#{atlasParam[:prefix]}_#{name}" | |
x_coord = x_dest + margin | |
y_coord = y_dest + margin | |
atlasParam[:header_fh].write "#define #{coordsName} Rect(#{x_coord},#{y_coord},#{w_dst},#{h_dst})\n" | |
atlasParam[:header_fh].write "#define #{scaleName} #{scaleDown}\n" | |
end | |
generateAtlas(node[:childa], atlasParam) if node[:childa] != 0 | |
generateAtlas(node[:childb], atlasParam) if node[:childb] != 0 | |
end | |
def usage | |
puts "mkatlas -f configfile [-s size] [-r imgrootdir] [-p pvrspacing]\n" | |
exit | |
end | |
def die(reason) | |
puts reason | |
exit 1 | |
end | |
$optparse = OptionParser.new do |opts| | |
opts.banner = "mkatlas -f configfile [-s size] [-r imgrootdir] [-p pvrspacing]" | |
opts.on("-f FILENAME", "--filename", "Filename") { |filename| $opts[:f] = filename } | |
opts.on("-s SIZE", "--size", "Size") { |size| $opts[:s] = size.to_i } | |
opts.on("-r IMGROOTDIR", "--root", "Image root") { |root| $opts[:r] = root } | |
opts.on("-p PVRSPACING", "--pvr-spacing", "PVR spacing") { |spacing| $opts[:p] = spacing.to_i } | |
end | |
def init | |
$optparse.parse! | |
usage if $opts[:h] or !$opts[:f] | |
$opts[:s] = 128 if $opts[:s].nil? | |
$opts[:r] = "" if $opts[:r].nil? | |
$opts[:p] = 0 if $opts[:p].nil? | |
die("Error: -p must be a multiple of 2") if $opts[:p] % 2 == 1 | |
puts "Options: s=#{$opts[:s]}, r=#{$opts[:r]}, p=#{$opts[:p]}" | |
end | |
init | |
main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment