Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image collection support with texture atlases #273

Merged
merged 6 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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