Skip to content

Commit

Permalink
Merge pull request #273 from coz-eduardo-hernandez/image_collections
Browse files Browse the repository at this point in the history
Image collection support with texture atlases
  • Loading branch information
karai17 authored Nov 4, 2022
2 parents 704c78c + db60f07 commit 2b1ada6
Show file tree
Hide file tree
Showing 6 changed files with 495 additions and 19 deletions.
3 changes: 2 additions & 1 deletion main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ local map, world, tx, ty, points

function love.load()
-- Load map
map = sti("tests/ortho.lua", { "box2d" })
map = sti("tests/collection.lua", { "box2d" })
--map = sti("tests/ortho.lua", { "box2d" })
--map = sti("tests/ortho-inf.lua", { "box2d" })
--map = sti("tests/iso.lua", { "box2d" })
--map = sti("tests/stag.lua", { "box2d" })
Expand Down
159 changes: 159 additions & 0 deletions sti/atlas.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---- Texture atlas complement for the Simple Tiled Implementation
-- @copyright 2022
-- @author Eduardo Hernández [email protected]
-- @license MIT/X11

local module = {}

--- Create a texture atlas
-- @param files Array with filenames
-- @param sort If "size" will sort by size, or if "id" will sort by id
-- @param ids Array with ids of each file
-- @param pow2 If true, will force a power of 2 size
function module.Atlas( files, sort, ids, pow2 )

local function Node(x, y, w, h)
return {x = x, y = y, w = w, h = h}
end

local function nextpow2( n )
local res = 1
while res <= n do
res = res * 2
end
return res
end

local function loadImgs()
local images = {}
for i = 1, #files do
images[i] = {}
--images[i].name = files[i]
if ids then images[i].id = ids[i] end
images[i].img = love.graphics.newImage( files[i] )
images[i].w = images[i].img:getWidth()
images[i].h = images[i].img:getHeight()
images[i].area = images[i].w * images[i].h
end
if sort == "size" or sort == "id" then
table.sort( images, function( a, b ) return ( a.area > b.area ) end )
end
return images
end

--TODO: understand this func
local function add(root, id, w, h)
if root.left or root.right then
if root.left then
local node = add(root.left, id, w, h)
if node then return node end
end
if root.right then
local node = add(root.right, id, w, h)
if node then return node end
end
return nil
end

if w > root.w or h > root.h then return nil end

local _w, _h = root.w - w, root.h - h

if _w <= _h then
root.left = Node(root.x + w, root.y, _w, h)
root.right = Node(root.x, root.y + h, root.w, _h)
else
root.left = Node(root.x, root.y + h, w, _h)
root.right = Node(root.x + w, root.y, _w, root.h)
end

root.w = w
root.h = h
root.id = id

return root
end

local function unmap(root)
if not root then return {} end

local tree = {}
if root.id then
tree[root.id] = {}
tree[root.id].x, tree[root.id].y = root.x, root.y
end

local left = unmap(root.left)
local right = unmap(root.right)

for k, v in pairs(left) do
tree[k] = {}
tree[k].x, tree[k].y = v.x, v.y
end
for k, v in pairs(right) do
tree[k] = {}
tree[k].x, tree[k].y = v.x, v.y
end

return tree
end

local function bake()
local images = loadImgs()

local root = {}
local w, h = images[1].w, images[1].h

if pow2 then
if w % 1 == 0 then w = nextpow2(w) end
if h % 1 == 0 then h = nextpow2(h) end
end

repeat
local node

root = Node(0, 0, w, h)

for i = 1, #images do
node = add(root, i, images[i].w, images[i].h)
if not node then break end
end

if not node then
if h <= w then
if pow2 then h = h * 2 else h = h + 1 end
else
if pow2 then w = w * 2 else w = w + 1 end
end
else
break
end
until false

local limits = love.graphics.getSystemLimits()
if w > limits.texturesize or h > limits.texturesize then
return "Resulting texture is too large for this system"
end

local coords = unmap(root)
local map = love.graphics.newCanvas(w, h)
love.graphics.setCanvas( map )
-- love.graphics.clear()

for i = 1, #images do
love.graphics.draw(images[i].img, coords[i].x, coords[i].y)
if ids then coords[i].id = images[i].id end
end
love.graphics.setCanvas()

if sort == "ids" then
table.sort( coords, function( a, b ) return ( a.id < b.id ) end )
end

return { image = map, coords = coords }
end

return bake()
end

return module
114 changes: 96 additions & 18 deletions sti/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ local utils = require(cwd .. "utils")
local ceil = math.ceil
local floor = math.floor
local lg = require(cwd .. "graphics")
local atlas = require(cwd .. "atlas")
local Map = {}
Map.__index = Map

Expand Down Expand Up @@ -96,21 +97,46 @@ function Map:init(path, plugins, ox, oy)
-- Set tiles, images
local gid = 1
for i, tileset in ipairs(self.tilesets) do
assert(tileset.image, "STI does not support Tile Collections.\nYou need to create a Texture Atlas.")

-- Cache images
if lg.isCreated then
local formatted_path = utils.format_path(path .. tileset.image)

if not STI.cache[formatted_path] then
utils.fix_transparent_color(tileset, formatted_path)
utils.cache_image(STI, formatted_path, tileset.image)
else
tileset.image = STI.cache[formatted_path]
end
end

gid = self:setTiles(i, tileset, gid)
assert(not tileset.filename, "STI does not support external Tilesets.\nYou need to embed all Tilesets.")

if tileset.image then
-- Cache images
if lg.isCreated then
local formatted_path = utils.format_path(path .. tileset.image)

if not STI.cache[formatted_path] then
utils.fix_transparent_color(tileset, formatted_path)
utils.cache_image(STI, formatted_path, tileset.image)
else
tileset.image = STI.cache[formatted_path]
end
end

gid = self:setTiles(i, tileset, gid)
elseif tileset.tilecount > 0 then
-- Build atlas for image collection
local files, ids = {}, {}
for j = 1, #tileset.tiles do
files[ j ] = utils.format_path(path .. tileset.tiles[j].image)
ids[ j ] = tileset.tiles[j].id
end

local map = atlas.Atlas( files, "ids", ids )

if lg.isCreated then
local formatted_path = utils.format_path(path .. tileset.name)

if not STI.cache[formatted_path] then
-- No need to fix transparency color for collections
utils.cache_image(STI, formatted_path, map.image)
tileset.image = map.image
else
tileset.image = STI.cache[formatted_path]
end
end

gid = self:setAtlasTiles(i, tileset, map.coords, gid)
end
end

local layers = {}
Expand Down Expand Up @@ -166,7 +192,7 @@ function Map:loadPlugins(plugins)
end
end

--- Create Tiles
--- Create Tiles based on a single tileset image
-- @param index Index of the Tileset
-- @param tileset Tileset data
-- @param gid First Global ID in Tileset
Expand Down Expand Up @@ -239,6 +265,59 @@ function Map:setTiles(index, tileset, gid)
return gid
end

--- Create Tiles based on a texture atlas
-- @param index Index of the Tileset
-- @param tileset Tileset data
-- @param coords Tile XY location in the atlas
-- @param gid First Global ID in Tileset
-- @return number Next Tileset's first Global ID
function Map:setAtlasTiles(index, tileset, coords, gid)
local quad = lg.newQuad
local imageW = tileset.image:getWidth()
local imageH = tileset.image:getHeight()

local firstgid = tileset.firstgid
for i = 1, #tileset.tiles do
local tile = tileset.tiles[i]
if tile.terrain then
terrain = {}

for j = 1, #tile.terrain do
terrain[j] = tileset.terrains[tile.terrain[j] + 1]
end
end

local tile = {
id = tile.id,
gid = firstgid + tile.id,
tileset = index,
class = tile.class,
quad = quad(
coords[i].x, coords[i].y,
tile.width, tile.height,
imageW, imageH
),
properties = tile.properties or {},
terrain = terrain,
animation = tile.animation,
objectGroup = tile.objectGroup,
frame = 1,
time = 0,
width = tile.width,
height = tile.height,
sx = 1,
sy = 1,
r = 0,
offset = tileset.tileoffset,
}

-- Be aware that in collections self.tiles can be a sparse array
self.tiles[tile.gid] = tile
end

return gid + #tileset.tiles
end

--- Create Layers
-- @param layer Layer data
-- @param path (Optional) Path to an Image Layer's image
Expand Down Expand Up @@ -398,9 +477,8 @@ function Map:getLayerTilePosition(layer, tile, x, y)
local tileX, tileY

if self.orientation == "orthogonal" then
local tileset = self.tilesets[tile.tileset]
tileX = (x - 1) * tileW + tile.offset.x
tileY = (y - 0) * tileH + tile.offset.y - tileset.tileheight
tileY = (y - 0) * tileH + tile.offset.y - tile.height
tileX, tileY = utils.compensate(tile, tileX, tileY, tileW, tileH)
elseif self.orientation == "isometric" then
tileX = (x - y) * (tileW / 2) + tile.offset.x + layer.width * tileW / 2 - self.tilewidth / 2
Expand Down
Loading

0 comments on commit 2b1ada6

Please sign in to comment.