* Removed any hotreload functionality to simplify the code-base, may introduce it back again if there's a need to redesign the UI as it was used for dynamic prototyping. * Split the code base into two packages: generator and frontend. The 'generator' package is reponsible for the functionality of making the atlases, etc... while the frontend is pure the immediate mode UI implemented with raygui and application globals * New build scripts, windows only for now.
591 lines
17 KiB
Odin
591 lines
17 KiB
Odin
package generator
|
|
|
|
import ase "../../vendors/aseprite"
|
|
import "core:encoding/json"
|
|
import "core:fmt"
|
|
import "core:log"
|
|
import "core:os"
|
|
import fp "core:path/filepath"
|
|
import "core:slice"
|
|
import "core:strings"
|
|
import rl "vendor:raylib"
|
|
import stbrp "vendor:stb/rect_pack"
|
|
|
|
when ODIN_OS == .Windows {
|
|
OS_FILE_SEPARATOR :: "\\"
|
|
} else {
|
|
OS_FILE_SEPARATOR :: "/"
|
|
}
|
|
|
|
CellData :: struct {
|
|
layer_index: u16,
|
|
opacity: u8,
|
|
frame_index: i32,
|
|
img: rl.Image,
|
|
}
|
|
|
|
AtlasEntry :: struct {
|
|
path: string,
|
|
cells: [dynamic]CellData,
|
|
frames: i32,
|
|
layer_names: [dynamic]string,
|
|
layer_cell_count: [dynamic]i32,
|
|
}
|
|
|
|
SpriteAtlasMetadata :: struct {
|
|
name: string,
|
|
location: [2]i32,
|
|
size: [2]i32,
|
|
}
|
|
|
|
SourceCodeGeneratorMetadata :: struct {
|
|
file_defines: struct {
|
|
top: string,
|
|
bottom: string,
|
|
file_name: string,
|
|
file_extension: string,
|
|
},
|
|
lanugage_settings: struct {
|
|
first_class_enum_arrays: bool, // for languages that support creating arrays that contain for each enum value an entry in the enum_data.entry_line: .EnumCase = {array entry}
|
|
},
|
|
custom_data_type: struct {
|
|
name: string,
|
|
type_declaration: string, // contains one param: custom_data_type.name + the rest of the type declaration like braces of the syntax & the type members
|
|
},
|
|
enum_data: struct {
|
|
name: string,
|
|
begin_line: string, // contains one params: enum_data.name
|
|
entry_line: string,
|
|
end_line: string,
|
|
},
|
|
array_data: struct {
|
|
name: string,
|
|
type: string,
|
|
begin_line: string, // array begin line contains 2 params in the listed order: array.name, array.type
|
|
entry_line: string, // array entry contains 5 params in the listed order: cell.name, cell.location.x, cell.location.y, cell.size.x, cell.size.y,
|
|
end_line: string,
|
|
},
|
|
}
|
|
|
|
unmarshall_aseprite_dir :: proc(
|
|
path: string,
|
|
atlas_entries: ^[dynamic]AtlasEntry,
|
|
alloc := context.allocator,
|
|
) {
|
|
if len(path) == 0 do return
|
|
|
|
if dir_fd, err := os.open(path, os.O_RDONLY); err == os.ERROR_NONE {
|
|
fis: []os.File_Info
|
|
if fis, err = os.read_dir(dir_fd, -1); err == os.ERROR_NONE {
|
|
unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc)
|
|
}
|
|
} else {
|
|
log.errorf("Couldn't open folder: ", path)
|
|
}
|
|
}
|
|
|
|
unmarshall_aseprite_files_file_info :: proc(
|
|
files: []os.File_Info,
|
|
atlas_entries: ^[dynamic]AtlasEntry,
|
|
alloc := context.allocator,
|
|
) {
|
|
if len(files) == 0 do return
|
|
|
|
paths := make([]string, len(files), alloc)
|
|
defer delete(paths)
|
|
|
|
for f, fi in files {
|
|
paths[fi] = f.fullpath
|
|
}
|
|
|
|
unmarshall_aseprite_files(paths[:], atlas_entries, alloc)
|
|
|
|
}
|
|
|
|
unmarshall_aseprite_files :: proc(
|
|
file_paths: []string,
|
|
atlas_entries: ^[dynamic]AtlasEntry,
|
|
alloc := context.allocator,
|
|
) {
|
|
if len(file_paths) == 0 do return
|
|
|
|
aseprite_document: ase.Document
|
|
for file in file_paths {
|
|
extension := fp.ext(file)
|
|
if extension != ".aseprite" do continue
|
|
|
|
log.infof("Unmarshalling file: ", file)
|
|
ase.unmarshal_from_filename(file, &aseprite_document, alloc)
|
|
atlas_entry := atlas_entry_from_compressed_cells(aseprite_document)
|
|
atlas_entry.path = file
|
|
|
|
append(atlas_entries, atlas_entry)
|
|
}
|
|
}
|
|
|
|
/*
|
|
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) {
|
|
atlas_entry.frames = auto_cast len(document.frames)
|
|
log.infof("N Frames: ", len(document.frames))
|
|
// NOTE(stefan): Since the expected input for the program is multiple files containing a single sprite
|
|
// it's probably a safe assumption most of the files will be a single layer with 1 or more frames
|
|
// which means we can first prod the file for information about how many frames are there and
|
|
// allocate a slice that is going to be [Frames X Layers]CellData.
|
|
// which would allow us to gain an already sorted list of sprites if we iterate all frames of a single layer
|
|
// instead of iterating all layers for each frame
|
|
// might be even quicker to first get that information an allocate at once the amount of cells we need
|
|
for frame, frameIdx in document.frames {
|
|
log.infof("Frame_{0} Chunks: ", frameIdx, len(frame.chunks))
|
|
for chunk in frame.chunks {
|
|
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
|
|
|
|
log.info(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 {
|
|
log.infof("Layer chunk: ", layer_chunk)
|
|
append(&atlas_entry.layer_names, layer_chunk.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
slice.sort_by(atlas_entry.cells[:], proc(i, j: CellData) -> bool {
|
|
return i.layer_index < j.layer_index
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
/*
|
|
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,
|
|
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_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_cell_images)
|
|
nodes := make([]stbrp.Node, num_entries, allocator)
|
|
rects := make([]stbrp.Rect, num_entries, allocator)
|
|
|
|
EntryAndCell :: struct {
|
|
entry: ^AtlasEntry,
|
|
cell_of_entry: ^CellData,
|
|
}
|
|
rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100, allocator)
|
|
|
|
// Set the custom IDs
|
|
cellIdx: int
|
|
for &entry, entryIdx in entries {
|
|
for &cell in entry.cells {
|
|
// I can probably infer this information with just the id of the rect but I'm being lazy right now
|
|
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 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
|
|
stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], i32(num_entries))
|
|
res := stbrp.pack_rects(&ctx, &rects[0], i32(num_entries))
|
|
if res == 1 {
|
|
log.info("Packed everything successfully!")
|
|
log.infof("Rects: {0}", rects[:])
|
|
} else {
|
|
log.error("Failed to pack everything!")
|
|
}
|
|
|
|
for rect, rectIdx in rects {
|
|
entry_and_cell := rect_idx_to_entry_and_cell[auto_cast rectIdx]
|
|
cell := entry_and_cell.cell_of_entry
|
|
|
|
src_rect := rl.Rectangle {
|
|
x = 0,
|
|
y = 0,
|
|
width = auto_cast cell.img.width,
|
|
height = auto_cast cell.img.height,
|
|
}
|
|
|
|
dst_rect := rl.Rectangle {
|
|
auto_cast rect.x + auto_cast offset_x,
|
|
auto_cast rect.y + auto_cast offset_y,
|
|
auto_cast cell.img.width,
|
|
auto_cast cell.img.height,
|
|
}
|
|
|
|
// note(stefan): drawing the sprite in the atlas in the packed coordinates
|
|
rl.ImageDraw(atlas, cell.img, src_rect, dst_rect, rl.WHITE)
|
|
|
|
log.infof("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
|
|
}
|
|
|
|
odin_source_generator_metadata := SourceCodeGeneratorMetadata {
|
|
file_defines = {
|
|
top = "package atlas_bindings\n\n",
|
|
bottom = "",
|
|
file_name = "metadata",
|
|
file_extension = ".odin",
|
|
},
|
|
custom_data_type = {
|
|
name = "AtlasRect",
|
|
type_declaration = "%v :: struct {{ x, y, w, h: i32 }}\n\n",
|
|
},
|
|
enum_data = {
|
|
name = "AtlasEnum",
|
|
begin_line = "%v :: enum {{\n",
|
|
entry_line = "\t%s,\n",
|
|
end_line = "}\n\n",
|
|
},
|
|
array_data = {
|
|
name = "ATLAS_SPRITES",
|
|
type = "[]AtlasRect",
|
|
begin_line = "%v := %v {{\n",
|
|
entry_line = "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n",
|
|
end_line = "}\n\n",
|
|
},
|
|
lanugage_settings = {first_class_enum_arrays = true},
|
|
}
|
|
|
|
|
|
cpp_source_generator_metadata := SourceCodeGeneratorMetadata {
|
|
file_defines = {
|
|
top = "#include <iostream>\n\n",
|
|
bottom = "",
|
|
file_name = "metadata",
|
|
file_extension = ".hpp",
|
|
},
|
|
custom_data_type = {
|
|
name = "AtlasRect",
|
|
type_declaration = "struct %v {{\n\tint x;\n\tint y;\n\tint w;\n\tint h;\n}};\n\n",
|
|
},
|
|
enum_data = {
|
|
name = "AtlasEnum",
|
|
begin_line = "enum %v {{\n",
|
|
entry_line = "\t%s,\n",
|
|
end_line = "\n\tCOUNT\n}\n\n",
|
|
},
|
|
array_data = {
|
|
name = "ATLAS_SPRITES",
|
|
type = "AtlasRect[size_t(AtlasEnum::COUNT)-1]",
|
|
begin_line = "{1} {0} = {{\n",
|
|
entry_line = "\t{{ {1}, {2}, {3}, {4} }},\n",
|
|
end_line = "}\n\n",
|
|
},
|
|
}
|
|
|
|
/*
|
|
Generates a barebones file with the package name "atlas_bindings",
|
|
the file contains an array of offsets, indexed by an enum.
|
|
The enum has unique names
|
|
*/
|
|
generate_odin_enums_and_atlas_offsets_file_sb :: proc(
|
|
metadata: []SpriteAtlasMetadata,
|
|
alloc := context.allocator,
|
|
) -> strings.Builder {
|
|
sb := strings.builder_make(alloc)
|
|
strings.write_string(&sb, "package atlas_bindings\n\n")
|
|
|
|
// Introduce the Rect type
|
|
strings.write_string(&sb, "AtlasRect :: struct { x, y, w, h: i32 }\n\n")
|
|
// start enum
|
|
strings.write_string(&sb, "AtlasSprite :: enum {\n")
|
|
{
|
|
for cell in metadata {
|
|
strings.write_string(&sb, fmt.aprintf("\t%s,\n", cell.name))
|
|
}
|
|
}
|
|
// end enum
|
|
strings.write_string(&sb, "}\n\n")
|
|
|
|
// start offsets array
|
|
// todo(stefan): the name of the array can be based on the output name?
|
|
strings.write_string(&sb, "ATLAS_SPRITES := []AtlasRect {\n")
|
|
{
|
|
entry: string
|
|
for cell in metadata {
|
|
entry = fmt.aprintf(
|
|
"\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n",
|
|
cell.name,
|
|
cell.location.x,
|
|
cell.location.y,
|
|
cell.size.x,
|
|
cell.size.y,
|
|
)
|
|
strings.write_string(&sb, entry)
|
|
}
|
|
}
|
|
// end offsets array
|
|
strings.write_string(&sb, "}\n\n")
|
|
|
|
log.info("\n", strings.to_string(sb))
|
|
|
|
return sb
|
|
}
|
|
|
|
metadata_source_code_generate :: proc(
|
|
metadata: []SpriteAtlasMetadata,
|
|
codegen: SourceCodeGeneratorMetadata,
|
|
alloc := context.allocator,
|
|
) -> strings.Builder {
|
|
|
|
sb := strings.builder_make(alloc)
|
|
// strings.write_string(&sb, "package atlas_bindings\n\n")
|
|
strings.write_string(&sb, codegen.file_defines.top)
|
|
|
|
// Introduce the Rect type
|
|
strings.write_string(
|
|
&sb,
|
|
fmt.aprintf(codegen.custom_data_type.type_declaration, codegen.custom_data_type.name),
|
|
)
|
|
// start enum
|
|
strings.write_string(&sb, fmt.aprintf(codegen.enum_data.begin_line, codegen.enum_data.name))
|
|
{
|
|
for cell in metadata {
|
|
strings.write_string(&sb, fmt.aprintf(codegen.enum_data.entry_line, cell.name))
|
|
}
|
|
}
|
|
// end enum
|
|
strings.write_string(&sb, codegen.enum_data.end_line)
|
|
|
|
// start offsets array
|
|
strings.write_string(
|
|
&sb,
|
|
fmt.aprintf(
|
|
codegen.array_data.begin_line,
|
|
codegen.array_data.name,
|
|
codegen.array_data.type,
|
|
),
|
|
)
|
|
{
|
|
entry: string
|
|
for cell in metadata {
|
|
entry = fmt.aprintf(
|
|
codegen.array_data.entry_line, // "\t.%v = {{ x = %v, y = %v, w = %v, h = %v }},\n",
|
|
cell.name,
|
|
cell.location.x,
|
|
cell.location.y,
|
|
cell.size.x,
|
|
cell.size.y,
|
|
)
|
|
strings.write_string(&sb, entry)
|
|
}
|
|
}
|
|
// end offsets array
|
|
|
|
strings.write_string(&sb, codegen.array_data.end_line)
|
|
|
|
strings.write_string(&sb, codegen.file_defines.bottom)
|
|
|
|
log.info("\n", strings.to_string(sb))
|
|
|
|
return sb
|
|
|
|
}
|
|
|
|
save_output :: proc(
|
|
output_folder_path: Maybe(string),
|
|
atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata),
|
|
atlas_render_texture_target: rl.RenderTexture2D,
|
|
) {
|
|
output_path, ok := output_folder_path.(string)
|
|
if !ok || output_path == "" {
|
|
log.error("Output path is empty!")
|
|
return
|
|
}
|
|
|
|
image := rl.LoadImageFromTexture(atlas_render_texture_target.texture)
|
|
rl.ImageFlipVertical(&image)
|
|
|
|
cstring_atlas_output_path := strings.clone_to_cstring(
|
|
strings.concatenate({output_path, OS_FILE_SEPARATOR, "atlas.png"}),
|
|
)
|
|
|
|
rl.ExportImage(image, cstring_atlas_output_path)
|
|
|
|
if metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); ok {
|
|
log.info("Building metadata...")
|
|
if json_metadata, jok := json.marshal(metadata); jok == nil {
|
|
os.write_entire_file(
|
|
strings.concatenate({output_path, OS_FILE_SEPARATOR, "metadata.json"}),
|
|
json_metadata,
|
|
)
|
|
} else {
|
|
log.error("Failed to marshall the atlas metadata to a json!")
|
|
}
|
|
|
|
// TODO(stefan): Think of a more generic alternative to just straight output to a odin file
|
|
// maybe supply a config.json that defines the start, end, line by line entry and enum format strings
|
|
// this way you can essentially support any language
|
|
sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:])
|
|
odin_metadata := strings.to_string(sb)
|
|
ok := os.write_entire_file(
|
|
strings.concatenate({output_path, OS_FILE_SEPARATOR, "metadata.odin"}),
|
|
transmute([]byte)odin_metadata,
|
|
)
|
|
if !ok {
|
|
log.error("Failed to save 'metadata.odin'")
|
|
}
|
|
} else {
|
|
log.error("No metadata to export!")
|
|
}
|
|
|
|
}
|
|
|
|
save_metadata_simple :: proc(
|
|
output_path: string,
|
|
json_file_name: Maybe(string),
|
|
source_file_name: Maybe(string),
|
|
source_gen_metadata: Maybe(SourceCodeGeneratorMetadata),
|
|
atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata),
|
|
) {
|
|
json_file_base_name, json_file_name_ok := json_file_name.(string)
|
|
source_file_base_name, source_file_name_ok := source_file_name.(string)
|
|
|
|
if !json_file_name_ok && !source_file_name_ok {
|
|
log.error("Neither a json file name or a source code filename has been provided!")
|
|
return
|
|
}
|
|
|
|
metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok {
|
|
log.error("No metadata to export!")
|
|
}
|
|
|
|
log.info("Building metadata...")
|
|
if json_file_name_ok {
|
|
if json_metadata, jok := json.marshal(metadata); jok == nil {
|
|
json_output_path := strings.concatenate(
|
|
{output_path, OS_FILE_SEPARATOR, json_file_base_name},
|
|
)
|
|
if ok = os.write_entire_file(json_output_path, json_metadata); !ok {
|
|
log.errorf("Failed to write json to file: ", json_output_path)
|
|
}
|
|
} else {
|
|
log.error("Failed to marshall the atlas metadata to a json!")
|
|
}
|
|
}
|
|
|
|
// note(stefan): Having source_file_name & source_gen_metadata is redundant but this is fine for now
|
|
if source_file_name_ok {
|
|
// if src_gen_metadata
|
|
if codegen, cok := source_gen_metadata.(SourceCodeGeneratorMetadata); cok {
|
|
sb := metadata_source_code_generate(metadata[:], codegen)
|
|
|
|
source_metadata := strings.to_string(sb)
|
|
source_output_path := strings.concatenate(
|
|
{
|
|
output_path,
|
|
OS_FILE_SEPARATOR,
|
|
codegen.file_defines.file_name,
|
|
codegen.file_defines.file_extension,
|
|
},
|
|
)
|
|
ok := os.write_entire_file(source_output_path, transmute([]byte)source_metadata)
|
|
if !ok {
|
|
log.errorf("Failed to save source code to file:", source_output_path)
|
|
}
|
|
} else {
|
|
sb := metadata_source_code_generate(metadata[:], odin_source_generator_metadata)
|
|
odin_metadata := strings.to_string(sb)
|
|
source_output_path := strings.concatenate(
|
|
{output_path, OS_FILE_SEPARATOR, "metadata.odin"},
|
|
)
|
|
|
|
ok := os.write_entire_file(source_output_path, transmute([]byte)odin_metadata)
|
|
if !ok {
|
|
log.errorf("Failed to save source code to file:", source_output_path)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
save_metadata :: proc(
|
|
settings: CLIPackerSettings,
|
|
atlas_entries: []AtlasEntry,
|
|
atlas_metadata: []SpriteAtlasMetadata,
|
|
) {
|
|
metadata, ok := settings.metadata.(CLIMetadataSettings);if !ok do return
|
|
|
|
if json_path, ok := metadata.json_path.(string); ok {
|
|
json_bytes, jerr := json.marshal(atlas_metadata)
|
|
if jerr == nil {
|
|
os.write_entire_file(json_path, json_bytes)
|
|
} else {
|
|
log.error("Failed to marshall metadata")
|
|
}
|
|
}
|
|
if source_code_path, ok := metadata.source_code_path.(string); ok {
|
|
sb := metadata_source_code_generate(atlas_metadata, odin_source_generator_metadata)
|
|
source_code_output_str := strings.to_string(sb)
|
|
os.write_entire_file(source_code_path, transmute([]byte)source_code_output_str)
|
|
}
|
|
}
|