From 67d7b3e2276c6a7048335ec34c22d9d41edae85f Mon Sep 17 00:00:00 2001 From: Stefan Stefanov Date: Tue, 23 Apr 2024 19:17:38 +0300 Subject: [PATCH] working demo of what the metdata exporter is supposed to do --- .vscode/tasks.json | 4 +- odinfmt.json | 6 + .../aseprite_odin_generator.odin | 12 +- src/aseprite_odin_generator/atlas.png | Bin 10891 -> 11027 bytes src/aseprite_odin_generator/big.aseprite | Bin 1115 -> 1333 bytes src/aseprite_odin_generator/metadata.json | 1 + src/generator.odin | 175 +++++++++++++----- src/globals.odin | 1 - src/save_output.odin | 4 +- src/symbol_exports.odin | 1 - 10 files changed, 150 insertions(+), 54 deletions(-) create mode 100644 odinfmt.json create mode 100644 src/aseprite_odin_generator/metadata.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e054793..a419bf5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -70,7 +70,7 @@ }, "group": { "kind": "build", - "isDefault": true + "isDefault": false }, }, { @@ -92,7 +92,7 @@ }, "group": { "kind": "build", - "isDefault": false + "isDefault": true }, } ] diff --git a/odinfmt.json b/odinfmt.json new file mode 100644 index 0000000..12c1d35 --- /dev/null +++ b/odinfmt.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json", + "character_width": 100, + "sort_imports": true, + "tabs": true +} \ No newline at end of file diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/src/aseprite_odin_generator/aseprite_odin_generator.odin index d897541..225578e 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/src/aseprite_odin_generator/aseprite_odin_generator.odin @@ -1,14 +1,17 @@ package generator import ase "../aseprite" +import "core:encoding/json" import "core:fmt" import "core:mem" import "core:os" import fp "core:path/filepath" import "core:slice" -import "core:strings" +import s "core:strings" import "core:testing" + import rl "vendor:raylib" +import stbrp "vendor:stb/rect_pack" import gen ".." @@ -24,14 +27,17 @@ main :: proc() { } cwd := os.get_current_directory() - target_dir := strings.concatenate({cwd, "\\src\\aseprite_odin_generator\\"}) + target_dir := s.concatenate({cwd, "\\src\\aseprite_odin_generator\\"}) atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) atlas_entries: [dynamic]gen.AtlasEntry gen.unmarshall_aseprite_dir(target_dir, &atlas_entries) + metadata := gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10) + + json_bytes, jerr := json.marshal(metadata) + os.write_entire_file("src/aseprite_odin_generator/metadata.json", json_bytes) - gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10) rl.ExportImage(atlas, EXPORT_PATH) } diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png index afb2d717bfef7045898ca774d537aaa5911f7546..94faceeb56095bd4d1e706f0f473f24853e86a14 100644 GIT binary patch literal 11027 zcmeI2Yfw{X8pq!gLOftXf`WTUfDkE&T4;o*fMNoH6Y&BRD7Dzth@%^wdyzJryFNl(&Ci^A?g0tWDn=|Lj$(cMk z&+Y#_zc-V~o|GgR%bn{E0AMA~k52`F25)HqWx$t#E$u@9OzTXHk4@jQY$fJO2!veP z`+dn}A{Q@A>g-8swd$V)hnTN|yi@&GCHsiDbiUKy)UU6DsIKQ!>{Ap3ZG&P-oidA>Y* zjc5@*T@AgR6Bxoj4`gqpgPx(CWYZij?Xmzoy03q=PS)0}Ky?~}0%Z~$?2jKwBEwfm zt;7p!$A2s`Kmxe(2_9dTjs-DqCPyb0=gJ>qC9 zJMmTTi?=X=^5zS$85tAcSmjF9*`j_7!~5qO6zlm)53Ji&m>F(Q8(vViypm{lVdv{9 zCqJFj+2+^xR-eIuk9Eb#bLoIf1a9z>*AME?r}AQ+udlD*+mmvGE#bBXM$qx;s`f3~ zm(R%Qs?YBPM)l}4R}{yh?3H~el~*oul`rj`J>d`5LjSi5GiUjK=k60~hD|r;8+uU# zx~Mhq^q{_`pX>L8ANq@3415i$n+5PCSM~c^4()B(k-M80k!JH6H0<@UR34d(+~(ck zhZ(JA&463bvIPSkH*_<&!9;-t_PU3I`5~C#n6hE;&C~~ni=ax%H$+(%Y*-Tq`x5BX zOZX?t#Qj2X)TZAFZ>yqNoY_}s(@>`YbLpDjyub|UYv~5_p}qg=C5)XN(=DX8g>W^& z4}5R;eerpWuhK$D-?lHVBZvUvlUvZ##gY3KZfESfCGL$km^i^r85Zf$63M!Rga@Jv zIam2`h%+CKgvo>>VN#`P2DFXO#8J{V!uY2ArV`HE-+rMs+_w?7Y>q zx9Q4*mwJ!vm9^&BBZSt!aFWG=3w013S6X*X`K-7t&9ARJtJZwQC&ni%FSQg;yp|K=SA6|o#LRGPY8vJewJrAAON40iB+70;!G7joGgx3r zv$!i`sB~!wyMO`Cdbg)kudSZpC(NuUa!3;dcbW%!PYlV8$2|s_=*>qeie< zD)0ExC7<<@eW`f|u}1~bWFv$9QteGMzVZe4qO9bDJdznPy0(zo)dJyU4_e%E5E&gU zJiu~3gl|K2mrH?6HV%(~Q#q1^?G7wJnS%-W#CRZ^_-3r>wp*U5(>|iXukDNPxCBh5 zYtp3h`hNc6euwV%14EL9GPSTe7rE%By?#d&@U=j=kL|;Sv;MB5iU7Lij>4e0FFtT@ zc-e-FR4;%J;4U~15Lb4*Q#wYI9`zJ<0&MXsXXMKQgNW;E6w@VT;=2sSMY6%)K`KYW z6)>?^AFIh*R>j6)UJTn2Zh=9*j&9rU(zm0P1K6r0L|3(EY|VD|ReV5aJPNUqLUGO+ zLc&J=P6qfia005bs&b#->ZKV-uX&@Ys6*+n5SQ{WdD)zwHDKnpr}>ZOkCk zu6I;i^17%ng^$LQd}EW0*|x8s#xwSW|D3~9b!5p-^3E3yH;md4 zcX|C@-r}wir*;oCI6{az=DN(dsy=e$Y=O!ARUkZv=7_o-i#EJ13iP%|9(gcqb%%|; zMLte}Jz*Ip!I7<^5Ag`6h=vpoWba2cv9;!q^XApnuKri8fvr8n&F#){+EM_T&mcvW z3}oiIAzrKGo#e1M^}`zo`x4`VqE;n3<;;b7h9j>$t*vVV z3t6K9cXQCBEbe$BW{A|iv-4xzy#HU^sAB$S�DAEuj{B^gyD9ff@#C7~cC|XsA<- z0vQD|3S<<>C=ya6q)14SkRl;PLW+bG2`Lg%B&0}4k&q%GMM8>%6bUI3QY55E_2D)c^nh literal 10891 zcmeI2|5FoZ9>)TWxJ<3>+2b^G)wyyLP^r~sWIgA^9Ho4-1kj2c-#mfblO;>&pKzW1 z<;hN-eV)(f^ZvX)ubIrGMypXvMByR;fFvV5H3t9$J|X}OgdY1s!cGT;FP(9?SQu<1VLmFmJ?HsCo_HnxR7?j`?Z*>e%jQjc4D# zS$8-(>+kdjhZRCXrPG?gZQcHt|J?uO_6H?pp=5WG3&)&!6C%Nm8`-5*_1!vbubBGi zJ|n{o=12!Op8X_}W)itqr|atUxM!6|iV_n=cz$=i_Z{&ew1sM3OPWPgJ*r(X z8uRAAztx9q@ON0OBD=p?7PB$lHa(+OgPU6=bPS-=Mts+c%TX!;skV^Jt;VA@zq1SQ z{0Xx!_)kzHLxBAaxC_`c#Zeb$)pf0GEp0L+lV%(akRO4&P)Fm7#L%4j;4Oc~En*`R z2>JrSrCR2yVf<2?{)xd3qP(hNugLL41ej}>ZJ;~jfPGq-uj2xa1qt3`(xW7T<8nOz zS?-z>XO4Z)sUL}C&v_SHE=p+vY=MfuHO(`9hPy8gxLZ8>rwip`D%~NGPEptogZFs! z{cIEA(iefyxq!Y9QHE06=g+R8m?97JH7Aj3t&lhmFlJI-lGIpK~3YVXq za#ZgFlyolGvs?41y|pRlS>^0{mp1I8jeTXU#2E4oz-r$0q>s!Ed^+>jJll0esQcGX z`w!gFKUC(AS-f`A#t4b-@-^c-J?RA#YRyAsVj*dDy01#g?dPIn2i4vR2^A@lgHLAE z<7vvlxWR~|xqXV~i{5}!_F4eB>w#q=_9PX75i378zgO(%*q97UfZY%o6nShwZh}y$ zu_5_hw)11yhv?WXTvanBN6Q}(QZi{7ym68a`#me9Z$YaZErlj`Uz-$Prnm+wIVIL@ye40VGi5v{Pq#u|n`GYy6VycB9m) z$m?SU#qbYOu9g@Zx@6enIm@0I&Q9JNNf@ORjw>MEnd2Jk)z((!NWYS;gR^UWG$#mgnHw<0{J|2&5O{h>L zangEo01a|{Cm|q?dFYGYU>-KC}`NSLtBAv^*hF^m7D8PmN5!jhm4>*!EqCO{v=6gCJ3&iMGdmM}g;)MkNT`9S{F7$lhUn9K4bJxM zOzzz-+n1po`o32e)k(=`g|Db>`&YFs&+@0v&<=Nc8;727WFiO>&!I1+3m|01=$gG; z6)N2h*tRR{Dip&kuvziM78#a=>J>j<+7_1h;Hn&VJ&(?MjVqMRpdD5_9-TGr|56nz zto{(eE)EheO)vEV<&Z(mb(RUHJSQLJtA$($Sf(ctCiOMOX^?X5o=C<;dSv(GylYohWEz=?*m=~-Us{`@F?I( z$diyKAx}b{gggm(67nSENyw9sCm~Nlo`gIJc@pv@?Gyo=Pk|XMf(LN>4x1bde4KrwDfi delta 71 zcmdnWb(@1dnuUSk!BWPF?3IkSH#V{}vil@frWPp}PA*`SfU=E1Y!QgK@eKwB237?I Q25trghX0#yFg7v)0Q#R4GXMYp diff --git a/src/aseprite_odin_generator/metadata.json b/src/aseprite_odin_generator/metadata.json new file mode 100644 index 0000000..a07b92b --- /dev/null +++ b/src/aseprite_odin_generator/metadata.json @@ -0,0 +1 @@ +[{"name":"Edinica","location":[95,10],"size":[58,57]},{"name":"Dvoika_0","location":[234,10],"size":[55,31]},{"name":"Troika","location":[10,10],"size":[75,75]},{"name":"Dvoika_1","location":[163,10],"size":[61,33]}] \ No newline at end of file diff --git a/src/generator.odin b/src/generator.odin index f1ad8e3..62483a8 100644 --- a/src/generator.odin +++ b/src/generator.odin @@ -1,15 +1,41 @@ package game import ase "./aseprite" +import "core:encoding/json" import "core:fmt" +import "core:mem" import "core:os" import fp "core:path/filepath" +import "core:strings" import rl "vendor:raylib" import stbrp "vendor:stb/rect_pack" +CellData :: struct { + layer_index: u16, + opacity: u8, + frame_index: i32, + img: rl.Image, +} + AtlasEntry :: struct { - path: string, - cells: [dynamic]rl.Image, + path: string, + cells: [dynamic]CellData, + frames: i32, + layer_names: [dynamic]string, + layer_cell_count: [dynamic]i32, +} + +SingleFrameSprite :: distinct rl.Rectangle + +AnimatedSprite :: struct { + x, y: i32, + width, height: i32, +} + +SpriteAtlasMetadata :: struct { + name: string, + location: [2]i32, + size: [2]i32, } unmarshall_aseprite_dir :: proc(path: string, atlas_entries: ^[dynamic]AtlasEntry) { @@ -27,14 +53,22 @@ unmarshall_aseprite_files_file_info :: proc( files: []os.File_Info, atlas_entries: ^[dynamic]AtlasEntry, ) { - paths: [dynamic]string - for f in files { - append(&paths, f.fullpath) + if len(files) == 0 do return + + paths := make([]string, len(files)) + defer delete(paths) + + for f, fi in files { + paths[fi] = f.fullpath } + unmarshall_aseprite_files(paths[:], atlas_entries) + } unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic]AtlasEntry) { + if len(file_paths) == 0 do return + aseprite_document: ase.Document for file in file_paths { extension := fp.ext(file) @@ -43,6 +77,7 @@ unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic fmt.println("Unmarshalling file: ", file) ase.unmarshal_from_filename(file, &aseprite_document) atlas_entry := atlas_entry_from_compressed_cells(aseprite_document) + atlas_entry.path = file append(atlas_entries, atlas_entry) } @@ -52,26 +87,34 @@ unmarshall_aseprite_files :: proc(file_paths: []string, atlas_entries: ^[dynamic Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image */ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entry: AtlasEntry) { - for frame in document.frames { + atlas_entry.frames = auto_cast len(document.frames) + fmt.println("N Frames: ", len(document.frames)) + for frame, frameIdx in document.frames { + fmt.printfln("Frame_{0} Chunks: ", frameIdx, len(frame.chunks)) for chunk in frame.chunks { - cel_chunk, cok := chunk.(ase.Cel_Chunk) - if !cok { - continue - } + if cel_chunk, ok := chunk.(ase.Cel_Chunk); ok { + cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel) + if !ci_ok do continue - cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel) - if !ci_ok { - continue + fmt.println(cel_chunk.layer_index) + + cell := CellData { + img = rl.Image { + data = rawptr(&cel_img.pixel[0]), + width = auto_cast cel_img.width, + height = auto_cast cel_img.height, + format = .UNCOMPRESSED_R8G8B8A8, + }, + frame_index = auto_cast frameIdx, + opacity = cel_chunk.opacity_level, + layer_index = cel_chunk.layer_index, + } + append(&atlas_entry.cells, cell) + } + if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok { + fmt.println("Layer chunk: ", layer_chunk) + append(&atlas_entry.layer_names, layer_chunk.name) } - append( - &atlas_entry.cells, - rl.Image { - data = rawptr(&cel_img.pixel[0]), - width = auto_cast cel_img.width, - height = auto_cast cel_img.height, - format = .UNCOMPRESSED_R8G8B8A8, - }, - ) } } return @@ -80,26 +123,33 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr /* Takes in a slice of entries, an output texture and offsets (offset_x/y) */ -pack_atlas_entries :: proc(entries: []AtlasEntry, atlas: ^rl.Image, offset_x: i32, offset_y: i32) { - assert(atlas.width != 0, "This shouldn't be 0!") - assert(atlas.height != 0, "This shouldn't be 0!") +pack_atlas_entries :: proc( + entries: []AtlasEntry, + atlas: ^rl.Image, + offset_x: i32, + offset_y: i32, + allocator := context.allocator, +) -> [dynamic]SpriteAtlasMetadata { + assert(atlas.width != 0, "Atlas width shouldn't be 0!") + assert(atlas.height != 0, "Atlas height shouldn't be 0!") - all_entries: [dynamic]rl.Image // it's fine to store it like this, rl.Image just stores a pointer to the data - { - for entry in entries { - append(&all_entries, ..entry.cells[:]) + all_cell_images := make([dynamic]rl.Image, allocator) // it's fine to store it like this, rl.Image just stores a pointer to the data + for &entry in entries { + for cell in entry.cells { + append(&all_cell_images, cell.img) } + entry.layer_cell_count = make([dynamic]i32, len(entry.cells), allocator) } - num_entries := len(all_entries) - nodes := make([]stbrp.Node, num_entries) - rects := make([]stbrp.Rect, num_entries) + num_entries := len(all_cell_images) + nodes := make([]stbrp.Node, num_entries, allocator) + rects := make([]stbrp.Rect, num_entries, allocator) EntryAndCell :: struct { entry: ^AtlasEntry, - cell_of_entry: ^rl.Image, + cell_of_entry: ^CellData, } - rect_idx_to_entry_and_cell: map[int]EntryAndCell + rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100, allocator) // Set the custom IDs cellIdx: int @@ -109,13 +159,15 @@ pack_atlas_entries :: proc(entries: []AtlasEntry, atlas: ^rl.Image, offset_x: i3 map_insert(&rect_idx_to_entry_and_cell, cellIdx, EntryAndCell{&entry, &cell}) rects[cellIdx].id = auto_cast entryIdx cellIdx += 1 + + entry.layer_cell_count[cell.layer_index] += 1 } } - for entry, entryIdx in all_entries { - entry_stb_rect := &rects[entryIdx] - entry_stb_rect.w = stbrp.Coord(entry.width + offset_x) - entry_stb_rect.h = stbrp.Coord(entry.height + offset_y) + for cell_image, cell_index in all_cell_images { + entry_stb_rect := &rects[cell_index] + entry_stb_rect.w = stbrp.Coord(cell_image.width + offset_x) + entry_stb_rect.h = stbrp.Coord(cell_image.height + offset_y) } ctx: stbrp.Context @@ -131,22 +183,53 @@ pack_atlas_entries :: proc(entries: []AtlasEntry, atlas: ^rl.Image, offset_x: i3 for rect, rectIdx in rects { entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx] cell := entry_and_cell.cell_of_entry - // We're grabbing the whole cell (the image itself) + src_rect := rl.Rectangle { x = 0, y = 0, - width = auto_cast cell.width, - height = auto_cast cell.height, + width = auto_cast cell.img.width, + height = auto_cast cell.img.height, } - // Placing it in the atlas in the calculated offsets (in the packing step) + dst_rect := rl.Rectangle { auto_cast rect.x + auto_cast offset_x, auto_cast rect.y + auto_cast offset_y, - auto_cast cell.width, - auto_cast cell.height, + auto_cast cell.img.width, + auto_cast cell.img.height, } - fmt.printfln("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) - rl.ImageDraw(atlas, cell^, src_rect, dst_rect, rl.WHITE) + // note(stefan): drawing the sprite in the atlas in the packed coordinates + rl.ImageDraw(atlas, cell.img, src_rect, dst_rect, rl.WHITE) + + fmt.printfln("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) } + + metadata := make([dynamic]SpriteAtlasMetadata, allocator) + for rect, rectIdx in rects { + entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx] + entry := entry_and_cell.entry + cell := entry_and_cell.cell_of_entry + + cell_name : string + if entry.layer_cell_count[cell.layer_index] > 1 { + cell_name = fmt.aprintf( + "{0}_%d", + entry.layer_names[cell.layer_index], + cell.frame_index, + allocator, + ) + } else { + cell_name = entry.layer_names[cell.layer_index] + } + cell_metadata := SpriteAtlasMetadata { + name = cell_name, + location = { + auto_cast rect.x + auto_cast offset_x, + auto_cast rect.y + auto_cast offset_y, + }, + size = {auto_cast cell.img.width, auto_cast cell.img.height}, + } + append(&metadata, cell_metadata) + } + return metadata } diff --git a/src/globals.odin b/src/globals.odin index b2af01b..42a8b54 100644 --- a/src/globals.odin +++ b/src/globals.odin @@ -34,7 +34,6 @@ PackerSettings :: struct { pixel_padding_x_int: i32, pixel_padding_y_int: i32, padding_enabled: bool, - fix_pixel_bleeding: bool, output_json: bool, output_odin: bool, } diff --git a/src/save_output.odin b/src/save_output.odin index 34d1914..68363fe 100644 --- a/src/save_output.odin +++ b/src/save_output.odin @@ -22,5 +22,7 @@ save_output :: proc() { output_path := strings.concatenate({output_path, atlas_path}) cstring_output_path := strings.clone_to_cstring(output_path) rl.ExportImage(image, cstring_output_path) - } + } else { + fmt.println("Output path is empty!") + } } diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin index 8774c81..d5ada25 100644 --- a/src/symbol_exports.odin +++ b/src/symbol_exports.odin @@ -13,7 +13,6 @@ game_update :: proc() -> bool { game_init_window :: proc() { rl.SetConfigFlags({.WINDOW_RESIZABLE}) rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") - rl.SetWindowPosition(200, 200) rl.SetWindowMinSize(1400, 800) }