Skip to content

Instantly share code, notes, and snippets.

@jamesu
Created September 14, 2011 21:54
Show Gist options
  • Save jamesu/1217908 to your computer and use it in GitHub Desktop.
Save jamesu/1217908 to your computer and use it in GitHub Desktop.
Atlas generator
#!/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