Skip to content

Instantly share code, notes, and snippets.

@kyzentun
Created June 9, 2022 04:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kyzentun/22fee2615ce498fd9b495d4f7cebb918 to your computer and use it in GitHub Desktop.
Save kyzentun/22fee2615ce498fd9b495d4f7cebb918 to your computer and use it in GitHub Desktop.
Takes a Super Metroid sprite sheet and quarters each frame to create a broken looking sprite
gamelicense = "Compatible"
-- replace subcritical with some library that can be used to read/write a png.
require "subcritical"
sc.no_strict_path = true
-- replace pngload and pngdump with png read and write functions.
local pngload = assert(SC.Construct("PNGLoader"))
local pngdump = assert(SC.Construct("PNGDumper"))
local passed_path= ...
local img_path= scpath(passed_path, 3)
local source= assert(pngload:Load(img_path))
local src_w, src_h= source:GetSize()
-- Construct a sprite sheet the same size as the source to copy to.
local dest= sc.Construct("Graphic", src_w, src_h)
local function split_frame(x, y, w, h)
local hw = w * .5
local hh = h * .5
local mx = x + hw
local my = y + hh
-- replace dest:Copy with whatever copies from one sprite sheet to another
dest:Copy(source, x, y, hw, hh, mx, my)
dest:Copy(source, mx, y, hw, hh, x, my)
dest:Copy(source, x, my, hw, hh, mx, y)
dest:Copy(source, mx, my, hw, hh, x, y)
end
local oopsed = false
local function is_delimiter(r, g, b, a)
-- 0, 0, 127 is the frame around all sprites
-- is_delimiter returns 1 or 0 for some bit math later.
return (a >= .5 and r == 0 and g == 0 and b >= .48 and b <= .52) and 1 or 0
end
-- Each corner is recognized by its pattern of four bits.
local corner_names = {
-- 10
-- 00
[1] = 'inner_upper_left',
-- 01
-- 00
[2] = 'inner_upper_right',
-- 00
-- 10
[4] = 'inner_lower_left',
-- 11
-- 10
[7] = 'upper_left',
-- 00
-- 01
[8] = 'inner_lower_right',
-- 11
-- 01
[11] = 'upper_right',
-- 10
-- 11
[13] = 'lower_left',
-- 01
-- 11
[14] = 'lower_right',
}
-- short corner names are for recognizing shapes of frames.
local short_ctypes = {
['inner_upper_left'] = 'q',
['inner_upper_right'] = 'e',
['inner_lower_left'] = 'z',
['upper_left'] = 'Q',
['inner_lower_right'] = 'c',
['upper_right'] = 'E',
['lower_left'] = 'Z',
['lower_right'] = 'C',
}
local function get_corner_type(x, y)
-- replace GetSRGBPixel with whatever reads the r,g,b,a values from a single pixel of the png.
-- Multiplies the 0 or 1 returned by is_delimiter for the bit math mentioned earlier.
local ulbit = is_delimiter(source:GetSRGBPixel(x, y))
local urbit = is_delimiter(source:GetSRGBPixel(x+1, y))*2
local llbit = is_delimiter(source:GetSRGBPixel(x, y+1))*4
local lrbit = is_delimiter(source:GetSRGBPixel(x+1, y+1))*8
local sum = ulbit + urbit + llbit + lrbit
return sum
end
-- used_corners will make sure that we don't process a frame twice when it has two upleft corners.
local used_corners = {}
-- scan functions find the outline of a frame.
local scan_left
local scan_right
local scan_up
local scan_down
scan_left = function(sx, sy)
for x = sx, 0, -1 do
local ctype = corner_names[get_corner_type(x, sy)]
if ctype == 'lower_left' then
return x, sy, scan_up, ctype
elseif ctype == 'inner_lower_right' then
return x, sy, scan_down, ctype
end
end
return nil, nil, 'ran off'
end
scan_right = function(sx, sy)
for x = sx, src_w-2 do
local ctype = corner_names[get_corner_type(x, sy)]
if ctype == 'upper_right' then
return x, sy, scan_down, ctype
elseif ctype == 'inner_upper_left' then
return x, sy, scan_up, ctype
end
end
return nil, nil, 'ran off'
end
scan_up = function(sx, sy)
for y = sy, 0, -1 do
local ctype = corner_names[get_corner_type(sx, y)]
if ctype == 'upper_left' then
return sx, y, scan_right, ctype
elseif ctype == 'inner_lower_left' then
return sx, y, scan_left, ctype
end
end
return nil, nil, 'ran off'
end
scan_down = function(sx, sy)
for y = sy, src_h-2 do
local ctype = corner_names[get_corner_type(sx, y)]
if ctype == 'lower_right' then
return sx, y, scan_left, ctype
elseif ctype == 'inner_upper_right' then
return sx, y, scan_right, ctype
end
end
return nil, nil, 'ran off'
end
-- func_names is just an unused lookup in case I needed to debug handle_frame getting into a weird state.
local func_names = {
[scan_left] = 'scan_left',
[scan_right] = 'scan_right',
[scan_up] = 'scan_up',
[scan_down] = 'scan_down',
}
-- handle_frame is called when an upper left corner is found.
-- needs to find the outline of the frame and split it appropriately.
local function handle_frame(left, top)
local corners = {{left, top, 'upper_left'}}
local state = scan_right
local x, y = left, top
local corcode = 'Q'
while true do
used_corners[x+(y*src_w)] = true
local nc_x, nc_y, next_state, ctype = state(x, y)
if next_state == 'ran off' then
return
end
if nc_x == left and nc_y == top then
break
else
corcode = corcode .. short_ctypes[ctype]
corners[#corners+1] = {nc_x, nc_y, ctype}
x = nc_x
y = nc_y
state = next_state
end
end
local l
local t
local r
local b
if corcode == 'QECZ' then
-- typical rectangle
l = corners[1][1]
t = corners[1][2]
r = corners[3][1]
b = corners[3][2]
-- 1111 1111
-- 1011 1001
-- 1001 1011
-- 1111 1111
elseif corcode == 'QEeECZ' then
-- Some angle aiming frames.
l = corners[1][1]
t = corners[3][2]
r = corners[4][1]
b = corners[5][2]
elseif corcode == 'QECcCZ' then
-- Some angle aiming frames.
l = corners[1][1]
t = corners[1][2]
r = corners[5][1]
b = corners[5][2]
-- 1111 1111
-- 1001 1101
-- 1101 1001
-- 1111 1111
elseif corcode == 'QECZzZ' then
-- Some angle aiming frames.
l = corners[4][1]
t = corners[2][2]
r = corners[2][1]
b = corners[4][2]
elseif corcode == 'QECZQq' then
-- Some angle aiming frames.
l = corners[5][1]
t = corners[5][2]
r = corners[3][1]
b = corners[3][2]
-- 11111 11111 11111
-- 11011 11001 11001
-- 10001 10001 10001
-- 10001 10011 11001
-- 11111 11111 11111
elseif corcode == 'QEeECZQq' then
print('unused octrect '..left..','..top..': '..corcode)
elseif corcode == 'QECcCZQq' then
-- used in grapple
if corners[5][2] - corners[1][2] > corners[3][1] - corners[7][1] then
l = corners[1][1]
t = corners[1][2]
r = corners[5][1]
b = corners[5][2]
else
l = corners[7][1]
t = corners[7][2]
r = corners[3][1]
b = corners[3][2]
end
elseif corcode == 'QECZzZQq' then
l = corners[1][1]
t = corners[1][2]
r = corners[3][1]
b = corners[3][2]
-- 11111 11111 11111
-- 10011 10011 10001
-- 10001 10001 10001
-- 10011 11001 11011
-- 11111 11111 11111
elseif corcode == 'QEeECcCZ' then
l = corners[1][1]
t = corners[1][2]
r = corners[7][1]
b = corners[7][2]
elseif corcode == 'QEeECZzZ' then
if corners[6][2] - corners[2][2] > corners[4][1] - corners[8][1] then
l = corners[6][1]
t = corners[2][2]
r = corners[2][1]
b = corners[6][2]
else
l = corners[8][1]
t = corners[4][2]
r = corners[4][1]
b = corners[8][2]
end
elseif corcode == 'QECcCZzZ' then
print('unused octrect '..left..','..top..': '..corcode)
elseif corcode == 'QEeEeECcCcCZzZzZQqQq' or
corcode == 'QqzZzcCceEeq' then
-- screw attack
elseif corcode == 'QEeEeECZzZ' then
-- grapple
elseif corcode == 'QECcCcCZQq' then
-- grapple
elseif corcode == 'QECcCZQqQq' then
-- grapple
elseif corcode == 'QEeECZzZzZ' then
-- grapple
else
print('unknown shape '..left..','..top..': '..corcode)
end
if l and t and r and b then
split_frame(l+1, t+1, r-l, b-t)
end
return corners[2][1] - left
end
-- This while loop goes through the entire sheet, just looking for upper_left corners.
-- If it finds one that isn't already known part of a frame, it handles that frame.
local x, y = 0, 0
while y < src_h-1 do
x = 0
while x < src_w-1 do
if corner_names[get_corner_type(x, y)] == 'upper_left'
and not used_corners[x+(y*src_w)] then
local mv_x = handle_frame(x, y)
if not mv_x then
x = src_w
else
x = x + mv_x
end
else
x = x + 1
end
end
y = y + 1
end
-- replace pngdump:Dump with whatever writes the dest sheet to a file.
-- just gives the new file the same name as the old with "_split.png" appended.
local dest_file = io.open(passed_path:sub(1, -5)..'_split.png', 'wb')
pngdump:Dump(dest, dest_file, dest_file.write)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment