diff --git a/assetbake/main.odin b/assetbake/main.odin index 445e9e0..f96d58a 100644 --- a/assetbake/main.odin +++ b/assetbake/main.odin @@ -56,10 +56,12 @@ Char_Def :: struct { // Packed atlas pixels + per-frame rects (indexed like the combined frames list) Atlas :: struct { - pixels: []u8, - width: int, - height: int, - rects: [][4]int, // [x, y, w, h] per frame; sizes may differ across clips + pixels: []u8, + width: int, + height: int, + rects: [][4]int, + source_sizes: [][2]int, + trim_offsets: [][2]int, } // Where one clip's frames sit inside the packed atlas frame list @@ -95,6 +97,8 @@ bake_character :: proc(in_dir, out_dir: string) { atlas := pack_atlas(frames) defer delete(atlas.pixels) defer delete(atlas.rects) + defer delete(atlas.source_sizes) + defer delete(atlas.trim_offsets) ensure_dir(out_dir) @@ -283,22 +287,56 @@ destroy_frames :: proc(frames: []^image.Image) { delete(frames) } -// pack_atlas packs variable-size frames with stb_rect_pack and blits them into one sheet. -// Caller owns atlas.pixels and atlas.rects. +opaque_bounds :: proc(img: ^image.Image, alpha_min: u8 = 1) -> (x, y, w, h: int) { + iw, ih := int(img.width), int(img.height) + pixels := bytes.buffer_to_bytes(&img.pixels) + + min_x, min_y := iw, ih + max_x, max_y := -1, -1 + + for py in 0 ..< ih { + for px in 0 ..< iw { + i := (py * iw + px) * 4 + a := pixels[i + 3] + if a >= alpha_min { + if px < min_x do min_x = px + if py < min_y do min_y = py + if px > max_x do max_x = px + if py > max_y do max_y = py + } + } + } + + if max_x < min_x { + return 0, 0, 1, 1 + } + return min_x, min_y, max_x - min_x + 1, max_y - min_y + 1 +} + pack_atlas :: proc(frames: []^image.Image) -> Atlas { - PADDING :: 1 // 1px gap to reduce bleeding if filtered later + PADDING :: 1 pack_rects := make([]stbrp.Rect, len(frames)) defer delete(pack_rects) + trims := make([][4]int, len(frames)) + defer delete(trims) + source_sizes := make([][2]int, len(frames)) + trim_offsets := make([][2]int, len(frames)) + total_area := 0 max_w := 0 max_h := 0 for frame, i in frames { - w := frame.width + PADDING - h := frame.height + PADDING + tx, ty, tw, th := opaque_bounds(frame) + trims[i] = {tx, ty, tw, th} + source_sizes[i] = {int(frame.width), int(frame.height)} + trim_offsets[i] = {tx, ty} + + w := tw + PADDING + h := th + PADDING pack_rects[i] = stbrp.Rect { - id = c.int(i), // pack_rects may reorder; id maps back to frame index + id = c.int(i), w = stbrp.Coord(w), h = stbrp.Coord(h), } @@ -307,7 +345,6 @@ pack_atlas :: proc(frames: []^image.Image) -> Atlas { max_h = max(max_h, h) } - // Grow a square-ish atlas until everything fits side := max(max_w, max_h, int(math.ceil(math.sqrt(f64(total_area))))) atlas_w, atlas_h := 0, 0 packed := false @@ -326,10 +363,9 @@ pack_atlas :: proc(frames: []^image.Image) -> Atlas { os.exit(1) } - pixels := make([]u8, atlas_w * atlas_h * 4) // transparent + pixels := make([]u8, atlas_w * atlas_h * 4) rects := make([][4]int, len(frames)) - // Blit using each rect's id (array order may have changed during packing) for pr in pack_rects { if !pr.was_packed { fmt.eprintfln("frame %d was not packed", pr.id) @@ -337,27 +373,30 @@ pack_atlas :: proc(frames: []^image.Image) -> Atlas { } idx := int(pr.id) frame := frames[idx] - fw := frame.width - fh := frame.height + trim := trims[idx] + tx, ty, tw, th := trim[0], trim[1], trim[2], trim[3] dst_x := int(pr.x) dst_y := int(pr.y) src := bytes.buffer_to_bytes(&frame.pixels) - for y in 0 ..< fh { - src_row := src[y * fw * 4:(y + 1) * fw * 4] + src_w := int(frame.width) + for y in 0 ..< th { + src_row := src[((ty + y) * src_w + tx) * 4:][:tw * 4] dst_i := ((dst_y + y) * atlas_w + dst_x) * 4 copy(pixels[dst_i:], src_row) } - rects[idx] = {dst_x, dst_y, fw, fh} + rects[idx] = {dst_x, dst_y, tw, th} } fmt.printfln("packed atlas %dx%d", atlas_w, atlas_h) return Atlas { - pixels = pixels, - width = atlas_w, - height = atlas_h, - rects = rects, + pixels = pixels, + width = atlas_w, + height = atlas_h, + rects = rects, + source_sizes = source_sizes, + trim_offsets = trim_offsets, } } @@ -422,8 +461,8 @@ build_char_def :: proc( r := atlas.rects[slot] frame_defs[i] = Frame_Def { rect = r, - source_size = {r[2], r[3]}, - trim_offset = {0, 0}, // no trimming in v1 + source_size = atlas.source_sizes[slot], + trim_offset = atlas.trim_offsets[slot], } } diff --git a/assets_baked/characters/toad/toad.atlas.png b/assets_baked/characters/toad/toad.atlas.png index f26d742..f8fda57 100644 Binary files a/assets_baked/characters/toad/toad.atlas.png and b/assets_baked/characters/toad/toad.atlas.png differ diff --git a/assets_baked/characters/toad/toad.char.json b/assets_baked/characters/toad/toad.char.json index 44ff0fb..0acfd6c 100644 --- a/assets_baked/characters/toad/toad.char.json +++ b/assets_baked/characters/toad/toad.char.json @@ -14,274 +14,274 @@ "frames": [ { "rect": [ - 0, - 0, - 268, - 326 + 1253, + 630, + 204, + 311 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 + 9, + 14 ] }, { "rect": [ - 269, + 1256, 0, - 268, - 326 + 218, + 314 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 + 9, + 10 ] }, { "rect": [ - 538, - 0, - 268, - 326 + 1458, + 630, + 231, + 310 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 + 9, + 8 ] }, { "rect": [ - 807, - 0, - 268, - 326 + 643, + 943, + 243, + 295 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 + 10, + 7 ] }, { "rect": [ - 1076, - 0, - 268, - 326 + 881, + 1245, + 254, + 278 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 1345, - 0, - 268, - 326 - ], - "source_size": [ - 268, - 326 - ], - "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 1614, - 0, - 268, - 326 - ], - "source_size": [ - 268, - 326 - ], - "trim_offset": [ - 0, - 0 + 11, + 8 ] }, { "rect": [ 0, - 327, - 268, - 326 + 944, + 247, + 288 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 + 10, + 7 ] }, { "rect": [ - 269, - 327, - 268, - 326 + 1669, + 941, + 236, + 305 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 538, - 327, - 268, - 326 - ], - "source_size": [ - 268, - 326 - ], - "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 807, - 327, - 268, - 326 - ], - "source_size": [ - 268, - 326 - ], - "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 1076, - 327, - 268, - 326 - ], - "source_size": [ - 268, - 326 - ], - "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 1345, - 327, - 268, - 326 - ], - "source_size": [ - 268, - 326 - ], - "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 1614, - 327, - 268, - 326 - ], - "source_size": [ - 268, - 326 - ], - "trim_offset": [ - 0, - 0 + 9, + 8 ] }, { "rect": [ 0, - 654, - 268, - 326 + 316, + 223, + 313 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 + 9, + 9 ] }, { "rect": [ - 269, - 654, - 268, - 326 + 1458, + 941, + 210, + 310 ], "source_size": [ 268, 326 ], "trim_offset": [ - 0, - 0 + 9, + 13 ] }, { "rect": [ - 538, - 654, - 268, - 326 + 224, + 316, + 211, + 313 ], "source_size": [ 268, 326 ], "trim_offset": [ + 11, + 11 + ] + }, + { + "rect": [ + 1032, + 630, + 220, + 311 + ], + "source_size": [ + 268, + 326 + ], + "trim_offset": [ + 13, + 7 + ] + }, + { + "rect": [ + 1032, + 942, + 228, + 302 + ], + "source_size": [ + 268, + 326 + ], + "trim_offset": [ + 16, + 4 + ] + }, + { + "rect": [ 0, - 0 + 1233, + 237, + 286 + ], + "source_size": [ + 268, + 326 + ], + "trim_offset": [ + 18, + 2 + ] + }, + { + "rect": [ + 643, + 1239, + 237, + 286 + ], + "source_size": [ + 268, + 326 + ], + "trim_offset": [ + 18, + 2 + ] + }, + { + "rect": [ + 414, + 943, + 228, + 302 + ], + "source_size": [ + 268, + 326 + ], + "trim_offset": [ + 16, + 4 + ] + }, + { + "rect": [ + 1690, + 630, + 220, + 310 + ], + "source_size": [ + 268, + 326 + ], + "trim_offset": [ + 13, + 7 + ] + }, + { + "rect": [ + 436, + 316, + 210, + 313 + ], + "source_size": [ + 268, + 326 + ], + "trim_offset": [ + 11, + 11 ] } ] @@ -292,290 +292,290 @@ "frames": [ { "rect": [ - 807, - 654, - 213, - 319 + 414, + 630, + 205, + 312 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 6, + 7 ] }, { "rect": [ - 1021, - 654, - 213, - 319 + 647, + 316, + 206, + 313 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 5, + 6 ] }, { "rect": [ - 1235, - 654, - 213, - 319 + 854, + 316, + 206, + 313 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 5, + 6 ] }, { "rect": [ - 1449, - 654, - 213, - 319 + 1684, + 0, + 207, + 314 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 4, + 5 ] }, { "rect": [ - 1663, - 654, - 213, - 319 + 1256, + 315, + 207, + 314 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 4, + 5 ] }, { "rect": [ - 807, - 974, - 213, - 319 + 420, + 0, + 208, + 315 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 3, + 4 ] }, { "rect": [ - 1021, - 974, - 213, - 319 + 1475, + 0, + 208, + 314 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 3, + 4 ] }, { "rect": [ - 1235, - 974, - 213, - 319 + 629, + 0, + 208, + 315 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 1449, - 974, - 213, - 319 - ], - "source_size": [ - 213, - 319 - ], - "trim_offset": [ - 0, - 0 - ] - }, - { - "rect": [ - 1663, - 974, - 213, - 319 - ], - "source_size": [ - 213, - 319 - ], - "trim_offset": [ - 0, - 0 + 3, + 3 ] }, { "rect": [ 0, - 981, - 213, - 319 + 0, + 209, + 315 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 2, + 3 ] }, { "rect": [ - 214, - 981, - 213, - 319 + 210, + 0, + 209, + 315 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 2, + 3 ] }, { "rect": [ - 428, - 981, - 213, - 319 + 838, + 0, + 208, + 315 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 3, + 3 ] }, { "rect": [ - 642, - 1294, - 213, - 319 + 1047, + 0, + 208, + 315 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 3, + 4 ] }, { "rect": [ - 856, - 1294, - 213, - 319 + 1464, + 315, + 207, + 314 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 4, + 5 ] }, { "rect": [ - 1070, - 1294, - 213, - 319 + 1672, + 315, + 207, + 314 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 4, + 5 ] }, { "rect": [ - 1284, - 1294, - 213, - 319 + 0, + 630, + 206, + 313 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 5, + 6 ] }, { "rect": [ - 1498, - 1294, - 213, - 319 + 207, + 630, + 206, + 313 ], "source_size": [ 213, 319 ], "trim_offset": [ - 0, - 0 + 5, + 6 + ] + }, + { + "rect": [ + 620, + 630, + 205, + 312 + ], + "source_size": [ + 213, + 319 + ], + "trim_offset": [ + 6, + 7 + ] + }, + { + "rect": [ + 826, + 630, + 205, + 312 + ], + "source_size": [ + 213, + 319 + ], + "trim_offset": [ + 6, + 7 ] } ] diff --git a/engine/character_data.odin b/engine/character_data.odin index 03504fe..3e1f985 100644 --- a/engine/character_data.odin +++ b/engine/character_data.odin @@ -210,6 +210,22 @@ character_clip :: proc(data: ^Character_Data, clip_name: string) -> (clip: Clip_ return character, true } +character_frame :: proc( + data: ^Character_Data, + clip_name: string, + frame_index: int, +) -> ( + frame: Frame_Def, + ok: bool, +) { + if data == nil do return {}, false + clip, found := data.def.clips[clip_name] + if !found || frame_index < 0 || frame_index >= len(clip.frames) { + return {}, false + } + return clip.frames[frame_index], true +} + character_frame_rect :: proc( data: ^Character_Data, clip_name: string, @@ -218,10 +234,7 @@ character_frame_rect :: proc( rect: [4]int, ok: bool, ) { - clip, found := data.def.clips[clip_name] - if !found || frame_index < 0 || frame_index >= len(clip.frames) { - return {}, false - } - - return clip.frames[frame_index].rect, true + frame, found := character_frame(data, clip_name, frame_index) + if !found do return {}, false + return frame.rect, true } diff --git a/engine/sprite.odin b/engine/sprite.odin index d9f7f76..1e83b34 100644 --- a/engine/sprite.odin +++ b/engine/sprite.odin @@ -84,7 +84,7 @@ to_clip :: proc(px, py, sw, sh: f32) -> [2]f32 { draw_sprite :: proc(app: ^App, sprite: ^Sprite) { if app.cmd == nil || app.swapchain_texture == nil { - return // begin_frame may have skipped (minimized window, etc.) + return } if sprite == nil || sprite.data == nil || sprite.data.texture == nil { return @@ -93,37 +93,51 @@ draw_sprite :: proc(app: ^App, sprite: ^Sprite) { return } - rect, ok := character_frame_rect(sprite.data, sprite.clip, sprite.frame) + frame, ok := character_frame(sprite.data, sprite.clip, sprite.frame) if !ok { return } - fw := f32(rect[2]) - fh := f32(rect[3]) + src_w := f32(frame.source_size[0]) + src_h := f32(frame.source_size[1]) + if src_w <= 0 do src_w = f32(frame.rect[2]) + if src_h <= 0 do src_h = f32(frame.rect[3]) + fw := f32(frame.rect[2]) + fh := f32(frame.rect[3]) + trim_x := f32(frame.trim_offset[0]) + trim_y := f32(frame.trim_offset[1]) pivot := sprite.data.def.pivot - origin := sprite_quad_origin(sprite.position, {fw, fh}, pivot) - x0_px := origin.x - y0_px := origin.y + + canvas_top := sprite.position.y - src_h * pivot[1] + x0_px, y0_px: f32 + if sprite.flip_x { + canvas_left := sprite.position.x - (1.0 - pivot[0]) * src_w + x0_px = canvas_left + (src_w - trim_x - fw) + y0_px = canvas_top + trim_y + } else { + canvas_left := sprite.position.x - src_w * pivot[0] + x0_px = canvas_left + trim_x + y0_px = canvas_top + trim_y + } x1_px := x0_px + fw y1_px := y0_px + fh sw := f32(app.swapchain_w) sh := f32(app.swapchain_h) - p0 := to_clip(x0_px, y0_px, sw, sh) // top-left - p1 := to_clip(x1_px, y0_px, sw, sh) // top-right - p2 := to_clip(x1_px, y1_px, sw, sh) // bottom-right - p3 := to_clip(x0_px, y1_px, sw, sh) // bottom-left + p0 := to_clip(x0_px, y0_px, sw, sh) + p1 := to_clip(x1_px, y0_px, sw, sh) + p2 := to_clip(x1_px, y1_px, sw, sh) + p3 := to_clip(x0_px, y1_px, sw, sh) tex_w := f32(sprite.data.width) tex_h := f32(sprite.data.height) - u0 := f32(rect[0]) / tex_w - v0 := f32(rect[1]) / tex_h - u1 := f32(rect[0] + rect[2]) / tex_w - v1 := f32(rect[1] + rect[3]) / tex_h + u0 := f32(frame.rect[0]) / tex_w + v0 := f32(frame.rect[1]) / tex_h + u1 := f32(frame.rect[0] + frame.rect[2]) / tex_w + v1 := f32(frame.rect[1] + frame.rect[3]) / tex_h if sprite.flip_x do u0, u1 = u1, u0 - // Two triangles: (0,1,2) and (0,2,3) verts := [SPRITE_VERT_COUNT]Vertex { {pos = p0, uv = {u0, v0}}, {pos = p1, uv = {u1, v0}},