Browse Source

Fix env mapping. Fix texture importer

Panagiotis Christopoulos Charitos 5 years ago
parent
commit
8280d0bca6

BIN
engine_data/SplitSumIntegration.ankitex


BIN
engine_data/SplitSumIntegration.png


+ 2 - 4
shaders/LightFunctions.glsl

@@ -107,10 +107,8 @@ F32 V_Schlick(F32 roughness, F32 NoV, F32 NoL)
 
 Vec3 envBRDF(Vec3 specular, F32 roughness, texture2D integrationLut, sampler integrationLutSampler, F32 NoV)
 {
-	const F32 a = roughness * roughness;
-	const F32 a2 = a * a;
-	const Vec2 envBRDF = textureLod(integrationLut, integrationLutSampler, Vec2(a2, NoV), 0.0).xy;
-	return specular * envBRDF.x + /*min(1.0, 50.0 * specular.g) */ envBRDF.y;
+	const Vec2 envBRDF = textureLod(integrationLut, integrationLutSampler, Vec2(roughness, NoV), 0.0).xy;
+	return specular * envBRDF.x + min(1.0, 50.0 * specular.g) * envBRDF.y;
 }
 
 Vec3 diffuseLambert(Vec3 diffuse)

+ 91 - 135
tools/texture/convert_image.py

@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 
 # Copyright (C) 2009-2020, Panagiotis Christopoulos Charitos and contributors.
 # All rights reserved.
@@ -13,11 +13,26 @@ import struct
 import copy
 import tempfile
 import shutil
+from PIL import Image
+from ctypes import Structure, sizeof, c_int, c_char, c_byte, c_uint
+from io import BytesIO
+
+
+class CStruct(Structure):
+    """ C structure """
+
+    def to_bytearray(self):
+        s = BytesIO()
+        s.write(self)
+        s.seek(0)
+        return s.read()
+
+    def from_bytearray(self, bin):
+        s = BytesIO(bin)
+        s.seek(0)
+        s.readinto(self)
 
 
-#
-# Config
-#
 class Config:
     in_files = []
     out_file = ""
@@ -95,48 +110,33 @@ DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x00008000
 DDSCAPS2_VOLUME = 0x00200000
 
 
-class DdsHeader:
+class DdsHeader(CStruct):
     """ The header of a dds file """
 
-    _fields = [
-        ('dwMagic', '4s'),
-        ('dwSize', 'I'),
-        ('dwFlags', 'I'),
-        ('dwHeight', 'I'),
-        ('dwWidth', 'I'),
-        ('dwPitchOrLinearSize', 'I'),
-        ('dwDepth', 'I'),
-        ('dwMipMapCount', 'I'),
-        ('dwReserved1', '44s'),
+    _fields_ = (
+        ('dwMagic', c_char * 4),
+        ('dwSize', c_int),
+        ('dwFlags', c_int),
+        ('dwHeight', c_int),
+        ('dwWidth', c_int),
+        ('dwPitchOrLinearSize', c_int),
+        ('dwDepth', c_int),
+        ('dwMipMapCount', c_int),
+        ('dwReserved1', c_char * 44),
 
         # Pixel format
-        ('dwSize', 'I'),
-        ('dwFlags', 'I'),
-        ('dwFourCC', '4s'),
-        ('dwRGBBitCount', 'I'),
-        ('dwRBitMask', 'I'),
-        ('dwGBitMask', 'I'),
-        ('dwBBitMask', 'I'),
-        ('dwRGBAlphaBitMask', 'I'),
-        ('dwCaps1', 'I'),
-        ('dwCaps2', 'I'),
-        ('dwCapsReserved', '8s'),
-        ('dwReserved2', 'I')
-    ]
-
-    def __init__(self, buff):
-        buff_format = self.get_format()
-        items = struct.unpack(buff_format, buff)
-        for field, value in map(None, self._fields, items):
-            setattr(self, field[0], value)
-
-    @classmethod
-    def get_format(cls):
-        return '<' + ''.join([f[1] for f in cls._fields])
-
-    @classmethod
-    def get_size(cls):
-        return struct.calcsize(cls.get_format())
+        ('dwSize', c_int),
+        ('dwFlags', c_int),
+        ('dwFourCC', c_char * 4),
+        ('dwRGBBitCount', c_int),
+        ('dwRBitMask', c_int),
+        ('dwGBitMask', c_int),
+        ('dwBBitMask', c_int),
+        ('dwRGBAlphaBitMask', c_int),
+        ('dwCaps1', c_int),
+        ('dwCaps2', c_int),
+        ('dwCapsReserved', c_char * 8),
+        ('dwReserved2', c_int))
 
 
 #
@@ -236,7 +236,7 @@ def parse_commandline():
         choices=["default", "linear", "nearest"],
         help="texture filtering. Can be: default, linear, nearest")
 
-    parser.add_argument("--mips-count", type=int, default=0xFFFF, help="Max number of mipmaps")
+    parser.add_argument("--mip-count", type=int, default=0xFFFF, help="Max number of mipmaps")
 
     args = parser.parse_args()
 
@@ -260,7 +260,7 @@ def parse_commandline():
     else:
         assert 0, "See file"
 
-    if args.mips_count <= 0:
+    if args.mip_count <= 0:
         parser.error("Wrong number of mipmaps")
 
     config = Config()
@@ -274,7 +274,7 @@ def parse_commandline():
     config.store_uncompressed = args.store_uncompressed
     config.to_linear_rgb = args.to_linear_rgb
     config.filter = filter
-    config.mips_count = args.mips_count
+    config.mip_count = args.mip_count
 
     if args.store_etc:
         config.compressed_formats = config.compressed_formats | DC_ETC2
@@ -288,40 +288,19 @@ def parse_commandline():
 def identify_image(in_file):
     """ Return the size of the input image and the internal format """
 
-    color_format = CF_NONE
-
-    proc = subprocess.Popen(["identify", "-verbose", in_file], stdout=subprocess.PIPE)
-
-    stdout_str = proc.stdout.read()
-
-    # Make sure the colorspace is what we want
-    """reg = re.search(r"Colorspace: (.*)", stdout_str)
-    if not reg or (reg.group(1) != "RGB" and reg.group(1) != "sRGB"):
-        raise Exception("Something is wrong with the colorspace")"""
+    img = Image.open(in_file)
 
-    # Get the size of the iamge
-    reg = re.search(r"Geometry: ([0-9]*)x([0-9]*)\+", stdout_str)
-
-    if not reg:
-        raise Exception("Cannot extract size")
-
-    # Identify the color space
-    """if not re.search(r"red: 8-bit", stdout_str) or not re.search(r"green: 8-bit", stdout_str) or not re.search(
-            r"blue: 8-bit", stdout_str):
-        raise Exception("Incorrect channel depths")"""
-
-    if re.search(r"alpha: 8-bit", stdout_str):
+    if img.mode == "RGB":
+        color_format = CF_RGB8
+    elif img.mode == "RGBA":
         color_format = CF_RGBA8
-        color_format_str = "RGBA"
     else:
-        color_format = CF_RGB8
-        color_format_str = "RGB"
+        raise Exception("Wrong format %s" % img.mode)
 
     # print some stuff and return
-    printi("width: %s, height: %s color format: %s" % \
-      (reg.group(1), reg.group(2), color_format_str))
+    printi("width: %s, height: %s color format: %s" % (img.width, img.height, img.mode))
 
-    return (color_format, int(reg.group(1)), int(reg.group(2)))
+    return (color_format, img.width, img.height)
 
 
 def create_mipmaps(in_file, tmp_dir, width_, height_, color_format, to_linear_rgb, max_mip_count):
@@ -354,8 +333,8 @@ def create_mipmaps(in_file, tmp_dir, width_, height_, color_format, to_linear_rg
             args.append("RGB")
 
         # Add this because it will automatically convert gray-like images to grayscale TGAs
-        args.append("-type")
-        args.append("TrueColor")
+        #args.append("-type")
+        #args.append("TrueColor")
 
         # resize
         args.append("-resize")
@@ -455,60 +434,34 @@ def write_raw(tex_file, fname, width, height, color_format):
 
     printi("  Appending %s" % fname)
 
-    # Read and check the header
-    uncompressed_tga_header = struct.pack("BBBBBBBBBBBB", 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0)
-
-    in_file = open(fname, "rb")
-    tga_header = in_file.read(12)
-
-    if len(tga_header) != 12:
-        raise Exception("Failed reading TGA header")
-
-    if uncompressed_tga_header != tga_header:
-        raise Exception("Incorrect TGA header")
-
-    # Read the size and bpp
-    header6_buff = in_file.read(6)
-
-    if len(header6_buff) != 6:
-        raise Exception("Failed reading TGA header #2")
+    img = Image.open(fname)
+    if img.size[0] != width or img.size[1] != height:
+        raise Exception("Expecting different image size")
 
-    header6 = struct.unpack("BBBBBB", header6_buff)
+    if (img.mode == "RGB" and color_format != CF_RGB8) or (img.mode == "RGBA" and color_format != CF_RGBA8):
+        raise Exception("Expecting different image format")
 
-    img_width = header6[1] * 256 + header6[0]
-    img_height = header6[3] * 256 + header6[2]
-    img_bpp = header6[4]
-
-    if (color_format != CF_RGB8 or img_bpp != 24) and (color_format != CF_RGBA8 or img_bpp != 32):
-        raise Exception("Unexpected bpp")
-
-    if img_width != width or img_height != height:
-        raise Exception("Unexpected width or height")
-
-    # Read the data
-    data_size = width * height
     if color_format == CF_RGB8:
-        data_size *= 3
+        img = img.convert("RGB")
     else:
-        data_size *= 4
-
-    data = bytearray(in_file.read(data_size))
-
-    if len(data) != data_size:
-        raise Exception("Failed to read all data")
+        img = img.convert("RGBA")
+
+    data = bytearray()
+
+    for h in range(0, int(height)):
+        for w in range(0, int(width)):
+            if color_format == CF_RGBA8:
+                r, g, b, a = img.getpixel((w, h))
+                data.append(r)
+                data.append(g)
+                data.append(b)
+                data.append(a)
+            else:
+                r, g, b = img.getpixel((w, h))
+                data.append(r)
+                data.append(g)
+                data.append(b)
 
-    tmp = in_file.read(128)
-    if len(tmp) != 0:
-        printw("  File shouldn't contain more data")
-
-    # Swap colors
-    bpp = img_bpp / 8
-    for i in xrange(0, data_size, bpp):
-        temp = data[i]
-        data[i] = data[i + 2]
-        data[i + 2] = temp
-
-    # Write data to tex_file
     tex_file.write(data)
 
 
@@ -519,20 +472,21 @@ def write_s3tc(out_file, fname, width, height, color_format):
     printi("  Appending %s" % fname)
     in_file = open(fname, "rb")
 
-    header = in_file.read(DdsHeader.get_size())
+    header_bin = in_file.read(sizeof(DdsHeader()))
 
-    if len(header) != DdsHeader.get_size():
+    if len(header_bin) != sizeof(DdsHeader()):
         raise Exception("Failed to read DDS header")
 
-    dds_header = DdsHeader(header)
+    dds_header = DdsHeader()
+    dds_header.from_bytearray(header_bin)
 
     if dds_header.dwWidth != width or dds_header.dwHeight != height:
         raise Exception("Incorrect width")
 
-    if color_format == CF_RGB8 and dds_header.dwFourCC != "DXT1":
+    if color_format == CF_RGB8 and dds_header.dwFourCC != b"DXT1":
         raise Exception("Incorrect format. Expecting DXT1")
 
-    if color_format == CF_RGBA8 and dds_header.dwFourCC != "DXT5":
+    if color_format == CF_RGBA8 and dds_header.dwFourCC != b"DXT5":
         raise Exception("Incorrect format. Expecting DXT5")
 
     # Read and write the data
@@ -543,7 +497,7 @@ def write_s3tc(out_file, fname, width, height, color_format):
 
     data_size = (width / 4) * (height / 4) * block_size
 
-    data = in_file.read(data_size)
+    data = in_file.read(int(data_size))
 
     if len(data) != data_size:
         raise Exception("Failed to read DDS data")
@@ -618,7 +572,7 @@ def convert(config):
     # Create images
     for in_file in config.in_files:
         mips_fnames = create_mipmaps(in_file, config.tmp_dir, width, height, color_format, config.to_linear_rgb,
-                                     config.mips_count)
+                                     config.mip_count)
 
         # Create etc images
         if config.compressed_formats & DC_ETC2:
@@ -652,8 +606,10 @@ def convert(config):
     if header_padding_size != 88:
         raise Exception("Check the header")
 
+    padding = bytearray()
     for i in range(0, header_padding_size):
-        tex_file.write('\0')
+        padding.append(0)
+    tex_file.write(padding)
 
     # For each compression
     for compression in range(0, 3):
@@ -671,7 +627,7 @@ def convert(config):
 
                 # Write RAW
                 if compression == 0 and config.store_uncompressed:
-                    write_raw(tex_file, in_base_fname + ".tga", tmp_width, tmp_height, color_format)
+                    write_raw(tex_file, in_base_fname + ".png", tmp_width, tmp_height, color_format)
                 # Write S3TC
                 elif compression == 1 and (config.compressed_formats & DC_S3TC):
                     write_s3tc(tex_file, in_base_fname + ".dds", tmp_width, tmp_height, color_format)
@@ -710,8 +666,8 @@ def main():
     try:
         convert(config)
     finally:
-        #shutil.rmtree(config.tmp_dir)
-        i = 0
+        shutil.rmtree(config.tmp_dir)
+        #i = 0
 
     # Done
     printi("Done!")

+ 233 - 215
tools/texture/create_atlas.py

@@ -10,292 +10,310 @@ from PIL import Image, ImageDraw
 from math import *
 import os
 
+
 class SubImage:
-	image = None
-	image_name = ""
-	width = 0
-	height = 0
+    image = None
+    image_name = ""
+    width = 0
+    height = 0
+
+    mwidth = 0
+    mheight = 0
 
-	mwidth = 0
-	mheight = 0
+    atlas_x = 0xFFFFFFFF
+    atlas_y = 0xFFFFFFFF
 
-	atlas_x = 0xFFFFFFFF
-	atlas_y = 0xFFFFFFFF
 
 class Frame:
-	x = 0
-	y = 0
-	w = 0
-	h = 0
+    x = 0
+    y = 0
+    w = 0
+    h = 0
 
-	@classmethod
-	def diagonal(self):
-		return sqrt(self.w * self.w + self.h * self.h)
+    @classmethod
+    def diagonal(self):
+        return sqrt(self.w * self.w + self.h * self.h)
+
+    @classmethod
+    def area(self):
+        return self.w * self.h
 
-	@classmethod
-	def area(self):
-		return self.w * self.h
 
 class Context:
-	in_files = []
-	out_file = ""
-	margin = 0
-	bg_color = 0
-	rpath = ""
+    in_files = []
+    out_file = ""
+    margin = 0
+    bg_color = 0
+    rpath = ""
+
+    sub_images = []
+    atlas_width = 0
+    atlas_height = 0
+    mode = None
 
-	sub_images = []
-	atlas_width = 0
-	atlas_height = 0
-	mode = None
 
 def next_power_of_two(x):
-	return pow(2.0, ceil(log(x) / log(2)))
+    return pow(2.0, ceil(log(x) / log(2)))
+
 
 def printi(msg):
-	print("[I] %s" % msg)
+    print("[I] %s" % msg)
+
 
 def parse_commandline():
-	""" Parse the command line arguments """
+    """ Parse the command line arguments """
+
+    parser = argparse.ArgumentParser(
+        description="This program creates a texture atlas", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
 
-	parser = argparse.ArgumentParser(description = "This program creates a texture atlas",
-			formatter_class = argparse.ArgumentDefaultsHelpFormatter)
+    parser.add_argument(
+        "-i", "--input", nargs="+", required=True, help="specify the image(s) to convert. Seperate with space")
 
-	parser.add_argument("-i", "--input", nargs = "+", required = True,
-			help = "specify the image(s) to convert. Seperate with space")
+    parser.add_argument("-o", "--output", default="atlas.png", help="specify output PNG image.")
 
-	parser.add_argument("-o", "--output", default = "atlas.png", help = "specify output PNG image.")
+    parser.add_argument("-m", "--margin", type=int, default=0, help="specify the margin.")
 
-	parser.add_argument("-m", "--margin", type = int, default = 0, help = "specify the margin.")
+    parser.add_argument("-b", "--background-color", help="specify background of empty areas", default="ff00ff00")
 
-	parser.add_argument("-b", "--background-color", help = "specify background of empty areas", default = "ff00ff00")
+    parser.add_argument("-r", "--rpath", help="Path to append to the .ankiatex", default="")
 
-	parser.add_argument("-r", "--rpath", help = "Path to append to the .ankiatex", default = "")
+    args = parser.parse_args()
 
-	args = parser.parse_args()
+    ctx = Context()
+    ctx.in_files = args.input
+    ctx.out_file = args.output
+    ctx.margin = args.margin
+    ctx.bg_color = int(args.background_color, 16)
+    ctx.rpath = args.rpath
 
-	ctx = Context()
-	ctx.in_files = args.input
-	ctx.out_file = args.output
-	ctx.margin = args.margin
-	ctx.bg_color = int(args.background_color, 16)
-	ctx.rpath = args.rpath
+    if len(ctx.in_files) < 2:
+        parser.error("Not enough images")
 
-	if len(ctx.in_files) < 2:
-		parser.error("Not enough images")
+    return ctx
 
-	return ctx
 
 def load_images(ctx):
-	""" Load the images """
+    """ Load the images """
 
-	for i in ctx.in_files:
-		img = SubImage()
-		img.image = Image.open(i)
-		img.image_name = i
+    for i in ctx.in_files:
+        img = SubImage()
+        img.image = Image.open(i)
+        img.image_name = i
 
-		if ctx.mode == None:
-			ctx.mode = img.image.mode
-		else:
-			if ctx.mode != img.image.mode:
-				raise Exception("Image \"%s\" has a different mode: \"%s\"" % (i, img.image.mode))
+        if ctx.mode == None:
+            ctx.mode = img.image.mode
+        else:
+            if ctx.mode != img.image.mode:
+                raise Exception("Image \"%s\" has a different mode: \"%s\"" % (i, img.image.mode))
 
-		img.width = img.image.size[0]
-		img.height = img.image.size[1]
+        img.width = img.image.size[0]
+        img.height = img.image.size[1]
 
-		img.mwidth = img.width + ctx.margin
-		img.mheight = img.height + ctx.margin
+        img.mwidth = img.width + ctx.margin
+        img.mheight = img.height + ctx.margin
 
-		printi("Image \"%s\" loaded. Mode \"%s\"" % (i, img.image.mode))
+        printi("Image \"%s\" loaded. Mode \"%s\"" % (i, img.image.mode))
 
-		for simage in ctx.sub_images:
-			if os.path.basename(simage.image_name) == os.path.basename(i):
-				raise Exception("Cannot have images with the same base %s" % i)
+        for simage in ctx.sub_images:
+            if os.path.basename(simage.image_name) == os.path.basename(i):
+                raise Exception("Cannot have images with the same base %s" % i)
+
+        ctx.sub_images.append(img)
 
-		ctx.sub_images.append(img)
 
 def compute_atlas_rough_size(ctx):
-	for i in ctx.sub_images:
-		ctx.atlas_width += i.mwidth
-		ctx.atlas_height += i.mheight
+    for i in ctx.sub_images:
+        ctx.atlas_width += i.mwidth
+        ctx.atlas_height += i.mheight
+
+    ctx.atlas_width = next_power_of_two(ctx.atlas_width)
+    ctx.atlas_height = next_power_of_two(ctx.atlas_height)
 
-	ctx.atlas_width = next_power_of_two(ctx.atlas_width)
-	ctx.atlas_height = next_power_of_two(ctx.atlas_height)
 
 def sort_image_key_diagonal(img):
-	return img.width * img.width + img.height * img.height
+    return img.width * img.width + img.height * img.height
+
 
 def sort_image_key_biggest_side(img):
-	return max(img.width, img.height)
+    return max(img.width, img.height)
+
 
 def best_fit(img, crnt_frame, frame):
-	if img.mwidth > frame.w or img.mheight > frame.h:
-		return False
+    if img.mwidth > frame.w or img.mheight > frame.h:
+        return False
+
+    if frame.area() < crnt_frame.area():
+        return True
+    else:
+        return False
 
-	if frame.area() < crnt_frame.area():
-		return True
-	else:
-		return False
 
 def worst_fit(img, crnt_frame, new_frame):
-	if img.mwidth > new_frame.w or img.mheight > new_frame.h:
-		return False
+    if img.mwidth > new_frame.w or img.mheight > new_frame.h:
+        return False
+
+    if new_frame.area() > crnt_frame.area():
+        return True
+    else:
+        return False
 
-	if new_frame.area() > crnt_frame.area():
-		return True
-	else:
-		return False
 
 def closer_to_00(img, crnt_frame, new_frame):
-	if img.mwidth > new_frame.w or img.mheight > new_frame.h:
-		return False
+    if img.mwidth > new_frame.w or img.mheight > new_frame.h:
+        return False
 
-	new_dist = new_frame.x * new_frame.x + new_frame.y * new_frame.y
-	crnt_dist = crnt_frame.x * crnt_frame.x + crnt_frame.y * crnt_frame.y
+    new_dist = new_frame.x * new_frame.x + new_frame.y * new_frame.y
+    crnt_dist = crnt_frame.x * crnt_frame.x + crnt_frame.y * crnt_frame.y
+
+    if new_dist < crnt_dist:
+        return True
+    else:
+        return False
 
-	if new_dist < crnt_dist:
-		return True
-	else:
-		return False
 
 def place_sub_images(ctx):
-	""" Place the sub images in the atlas """
-
-	# Sort the images
-	ctx.sub_images.sort(key = sort_image_key_diagonal, reverse = True)
-
-	frame = Frame()
-	frame.w = ctx.atlas_width
-	frame.h = ctx.atlas_height
-	frames = []
-	frames.append(frame)
-
-	unplaced_imgs = []
-	for i in range(0, len(ctx.sub_images)):
-		unplaced_imgs.append(i)
-
-	while len(unplaced_imgs) > 0:
-		sub_image = ctx.sub_images[unplaced_imgs[0]]
-		unplaced_imgs.pop(0)
-
-		printi("Will try to place image \"%s\" of size %ux%d" %
-				(sub_image.image_name, sub_image.width, sub_image.height))
-
-		# Find best frame
-		best_frame = None
-		best_frame_idx = 0
-		idx = 0
-		for frame in frames:
-			if not best_frame or closer_to_00(sub_image, best_frame, frame):
-				best_frame = frame
-				best_frame_idx = idx
-			idx += 1
-
-		assert best_frame != None, "See file"
-
-		# Update the sub_image
-		sub_image.atlas_x = best_frame.x + ctx.margin
-		sub_image.atlas_y =	best_frame.y + ctx.margin
-		printi("Image placed in %dx%d" % (sub_image.atlas_x, sub_image.atlas_y))
-
-		# Split frame
-		frame_top = Frame()
-		frame_top.x = best_frame.x + sub_image.mwidth
-		frame_top.y = best_frame.y
-		frame_top.w = best_frame.w - sub_image.mwidth
-		frame_top.h = sub_image.mheight
-
-		frame_bottom = Frame()
-		frame_bottom.x = best_frame.x
-		frame_bottom.y = best_frame.y + sub_image.mheight
-		frame_bottom.w = best_frame.w
-		frame_bottom.h = best_frame.h - sub_image.mheight
-
-		frames.pop(best_frame_idx)
-		frames.append(frame_top)
-		frames.append(frame_bottom)
+    """ Place the sub images in the atlas """
+
+    # Sort the images
+    ctx.sub_images.sort(key=sort_image_key_diagonal, reverse=True)
+
+    frame = Frame()
+    frame.w = ctx.atlas_width
+    frame.h = ctx.atlas_height
+    frames = []
+    frames.append(frame)
+
+    unplaced_imgs = []
+    for i in range(0, len(ctx.sub_images)):
+        unplaced_imgs.append(i)
+
+    while len(unplaced_imgs) > 0:
+        sub_image = ctx.sub_images[unplaced_imgs[0]]
+        unplaced_imgs.pop(0)
+
+        printi("Will try to place image \"%s\" of size %ux%d" % (sub_image.image_name, sub_image.width,
+                                                                 sub_image.height))
+
+        # Find best frame
+        best_frame = None
+        best_frame_idx = 0
+        idx = 0
+        for frame in frames:
+            if not best_frame or closer_to_00(sub_image, best_frame, frame):
+                best_frame = frame
+                best_frame_idx = idx
+            idx += 1
+
+        assert best_frame != None, "See file"
+
+        # Update the sub_image
+        sub_image.atlas_x = best_frame.x + ctx.margin
+        sub_image.atlas_y = best_frame.y + ctx.margin
+        printi("Image placed in %dx%d" % (sub_image.atlas_x, sub_image.atlas_y))
+
+        # Split frame
+        frame_top = Frame()
+        frame_top.x = best_frame.x + sub_image.mwidth
+        frame_top.y = best_frame.y
+        frame_top.w = best_frame.w - sub_image.mwidth
+        frame_top.h = sub_image.mheight
+
+        frame_bottom = Frame()
+        frame_bottom.x = best_frame.x
+        frame_bottom.y = best_frame.y + sub_image.mheight
+        frame_bottom.w = best_frame.w
+        frame_bottom.h = best_frame.h - sub_image.mheight
+
+        frames.pop(best_frame_idx)
+        frames.append(frame_top)
+        frames.append(frame_bottom)
+
 
 def shrink_atlas(ctx):
-	""" Compute the new atlas size """
+    """ Compute the new atlas size """
+
+    width = 0
+    height = 0
+    for sub_image in ctx.sub_images:
+        width = max(width, sub_image.atlas_x + sub_image.width + ctx.margin)
+        height = max(height, sub_image.atlas_y + sub_image.height + ctx.margin)
 
-	width = 0
-	height = 0
-	for sub_image in ctx.sub_images:
-		width = max(width, sub_image.atlas_x + sub_image.width + ctx.margin)
-		height = max(height, sub_image.atlas_y + sub_image.height + ctx.margin)
+    ctx.atlas_width = next_power_of_two(width)
+    ctx.atlas_height = next_power_of_two(height)
 
-	ctx.atlas_width = next_power_of_two(width)
-	ctx.atlas_height = next_power_of_two(height)
 
 def create_atlas(ctx):
-	""" Create and populate the atlas """
+    """ Create and populate the atlas """
 
-	# Change the color to something PIL can understand
-	bg_color = (ctx.bg_color >> 24)
-	bg_color |= (ctx.bg_color >> 8) & 0xFF00
-	bg_color |= (ctx.bg_color << 8) & 0xFF0000
-	bg_color |= (ctx.bg_color << 24) & 0xFF000000
+    # Change the color to something PIL can understand
+    bg_color = (ctx.bg_color >> 24)
+    bg_color |= (ctx.bg_color >> 8) & 0xFF00
+    bg_color |= (ctx.bg_color << 8) & 0xFF0000
+    bg_color |= (ctx.bg_color << 24) & 0xFF000000
 
-	mode = "RGB"
-	if ctx.mode == "RGB":
-		color_space = (255, 255, 255)
-	else:
-		mode = "RGBA"
-		color_space = (255, 255, 255, 255)
+    mode = "RGB"
+    if ctx.mode == "RGB":
+        color_space = (255, 255, 255)
+    else:
+        mode = "RGBA"
+        color_space = (255, 255, 255, 255)
 
-	atlas_img = Image.new(mode, \
-			(int(ctx.atlas_width), int(ctx.atlas_height)), color_space)
+    atlas_img = Image.new(mode, \
+      (int(ctx.atlas_width), int(ctx.atlas_height)), color_space)
 
-	draw = ImageDraw.Draw(atlas_img)
-	draw.rectangle((0, 0, ctx.atlas_width, ctx.atlas_height), bg_color)
+    draw = ImageDraw.Draw(atlas_img)
+    draw.rectangle((0, 0, ctx.atlas_width, ctx.atlas_height), bg_color)
 
-	for sub_image in ctx.sub_images:
-		assert sub_image.atlas_x != 0xFFFFFFFF and sub_image.atlas_y != 0xFFFFFFFF, "See file"
+    for sub_image in ctx.sub_images:
+        assert sub_image.atlas_x != 0xFFFFFFFF and sub_image.atlas_y != 0xFFFFFFFF, "See file"
 
-		atlas_img.paste(sub_image.image, (int(sub_image.atlas_x), int(sub_image.atlas_y)))
+        atlas_img.paste(sub_image.image, (int(sub_image.atlas_x), int(sub_image.atlas_y)))
+
+    printi("Saving atlas \"%s\"" % ctx.out_file)
+    atlas_img.save(ctx.out_file)
 
-	printi("Saving atlas \"%s\"" % ctx.out_file)
-	atlas_img.save(ctx.out_file)
 
 def write_xml(ctx):
-	""" Write the schema """
-
-	fname = os.path.splitext(ctx.out_file)[0] + ".ankiatex"
-	printi("Writing XML \"%s\"" % fname)
-	f = open(fname, "w")
-	f.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n")
-	f.write("<textureAtlas>\n")
-	out_filename = ctx.rpath + "/" + os.path.splitext(os.path.basename(ctx.out_file))[0] + ".ankitex"
-	f.write("\t<texture>%s</texture>\n" % out_filename)
-	f.write("\t<subTextureMargin>%u</subTextureMargin>\n" % ctx.margin)
-	f.write("\t<subTextures>\n")
-
-	for sub_image in ctx.sub_images:
-		f.write("\t\t<subTexture>\n")
-		f.write("\t\t\t<name>%s</name>\n" % os.path.basename(sub_image.image_name))
-
-		# Now change coordinate system
-		left = sub_image.atlas_x / ctx.atlas_width
-		right = left + (sub_image.width / ctx.atlas_width)
-		top = (ctx.atlas_height - sub_image.atlas_y) / ctx.atlas_height
-		bottom = top - (sub_image.height / ctx.atlas_height)
-
-		f.write("\t\t\t<uv>%f %f %f %f</uv>\n" % (left, bottom, right, top))
-		f.write("\t\t</subTexture>\n")
-
-	f.write("\t</subTextures>\n")
-	f.write("</textureAtlas>\n")
+    """ Write the schema """
+
+    fname = os.path.splitext(ctx.out_file)[0] + ".ankiatex"
+    printi("Writing XML \"%s\"" % fname)
+    f = open(fname, "w")
+    f.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n")
+    f.write("<textureAtlas>\n")
+    out_filename = ctx.rpath + "/" + os.path.splitext(os.path.basename(ctx.out_file))[0] + ".ankitex"
+    f.write("\t<texture>%s</texture>\n" % out_filename)
+    f.write("\t<subTextureMargin>%u</subTextureMargin>\n" % ctx.margin)
+    f.write("\t<subTextures>\n")
+
+    for sub_image in ctx.sub_images:
+        f.write("\t\t<subTexture>\n")
+        f.write("\t\t\t<name>%s</name>\n" % os.path.basename(sub_image.image_name))
+
+        # Now change coordinate system
+        left = sub_image.atlas_x / ctx.atlas_width
+        right = left + (sub_image.width / ctx.atlas_width)
+        top = (ctx.atlas_height - sub_image.atlas_y) / ctx.atlas_height
+        bottom = top - (sub_image.height / ctx.atlas_height)
+
+        f.write("\t\t\t<uv>%f %f %f %f</uv>\n" % (left, bottom, right, top))
+        f.write("\t\t</subTexture>\n")
+
+    f.write("\t</subTextures>\n")
+    f.write("</textureAtlas>\n")
+
 
 def main():
-	""" The main """
+    """ The main """
 
-	ctx = parse_commandline()
-	load_images(ctx)
-	compute_atlas_rough_size(ctx)
-	place_sub_images(ctx)
-	shrink_atlas(ctx)
-	create_atlas(ctx)
-	write_xml(ctx)
+    ctx = parse_commandline()
+    load_images(ctx)
+    compute_atlas_rough_size(ctx)
+    place_sub_images(ctx)
+    shrink_atlas(ctx)
+    create_atlas(ctx)
+    write_xml(ctx)
 
-if __name__ == "__main__":
-	main()
 
+if __name__ == "__main__":
+    main()