Created
June 9, 2022 04:33
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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