changeset 414:0deefe1c8af6

add: 2d-packing algorithm for texture-atlas generation
author Sam <sam@basx.dev>
date Thu, 25 Jan 2024 20:23:22 +0700
parents 8fb4387b2ea1
children 25db1fa56cb7
files semicongine/algorithms.nim
diffstat 1 files changed, 95 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/semicongine/algorithms.nim	Thu Jan 25 20:23:22 2024 +0700
@@ -0,0 +1,95 @@
+import std/algorithm
+import std/sequtils
+
+import ./core
+
+type
+  Rect = tuple
+    i, x, y, w, h: int
+
+func between(a1, a2, b: int): bool =
+  a1 <= b and b <= a2
+
+func overlap(a1, a2, b1, b2: int): bool =
+  return between(a1, a2, b1) or
+         between(a1, a2, b2) or
+         between(b1, b2, a1) or
+         between(b1, b2, a2)
+
+# FYI: also serves as "overlaps"
+func advanceIfOverlap(fix, newRect: Rect): (bool, int) =
+  let
+    p1 = [
+      (fix.x,             fix.y),
+      (fix.x + fix.w - 1, fix.y),
+      (fix.x,             fix.y + fix.h - 1),
+      (fix.x + fix.w - 1, fix.y + fix.h - 1)
+    ]
+    p2 = [
+      (newRect.x,                 newRect.y),
+      (newRect.x + newRect.w - 1, newRect.y),
+      (newRect.x,                 newRect.y + newRect.h - 1),
+      (newRect.x + newRect.w - 1, newRect.y + newRect.h - 1)
+    ]
+  let overlapping = overlap(fix.x, fix.x + fix.w - 1, newRect.x, newRect.x + newRect.w - 1) and
+                    overlap(fix.y, fix.y + fix.h - 1, newRect.y, newRect.y + newRect.h - 1)
+  if overlapping: (true, fix.x + fix.w) # next free x coordinate to the right
+  else: (false, newRect.x) # current position is fine
+
+proc find_insertion_position(alreadyPlaced: seq[Rect], area: tuple[i, w, h: int], maxDim: int): (bool, Rect) =
+  var newRect = (i: area.i, x: 0, y: 0, w: area.w, h: area.h)
+
+  while newRect.y + newRect.h <= maxDim:
+    var hasOverlap = false
+    var advanceX: int
+
+    for placed in alreadyPlaced:
+      (hasOverlap, advanceX) = placed.advanceIfOverlap(newRect)
+      if hasOverlap: # rects were overlapping and newRect needs to be shifted to the right
+        newRect.x = advanceX
+        break
+
+    if not hasOverlap: # found a collision free position
+      return (true, newRect)
+
+    if newRect.x + newRect.w >= maxDim: # move to next scanline
+      newRect.x = 0
+      newRect.y += 1
+
+  return (false, newRect)
+
+
+proc pack*[T: Pixel](images: seq[Image[T]]): tuple[atlas: Image[T], coords: seq[tuple[x: int, y: int]]] =
+  const MAX_ATLAS_SIZE = 4096
+  var areas: seq[tuple[i, w, h: int]]
+
+  for i in 0 ..< images.len:
+    areas.add (i, images[i].width, images[i].height)
+
+  let areasBySize = areas.sortedByIt(-(it[1] * it[2]))
+  var assignedAreas: seq[Rect]
+  var maxDim = 128
+
+  for area in areasBySize:
+    var pos = find_insertion_position(assignedAreas, area, maxDim)
+    while not pos[0]: # this should actually never loop more than once, but weird things happen ¯\_(ツ)_/¯ 
+      maxDim = maxDim * 2
+      assert maxDim <= MAX_ATLAS_SIZE, &"Atlas gets bigger than {MAX_ATLAS_SIZE}, cannot pack images"
+      pos = find_insertion_position(assignedAreas, area, maxDim)
+
+    assignedAreas.add pos[1]
+
+  # check there are overlaps
+  for i in 0 ..< assignedAreas.len - 1:
+    for j in i + 1 ..< assignedAreas.len:
+      assert not assignedAreas[i].advanceIfOverlap(assignedAreas[j])[0], &"{assignedAreas[i]} and {assignedAreas[j]} overlap!"
+
+  result.atlas = newImage[T](maxDim, maxDim)
+  result.coords.setLen(images.len)
+  for rect in assignedAreas:
+    for y in 0 ..< rect.h:
+      for x in 0 ..< rect.w:
+        let v = images[rect.i][x, y]
+        assert result.atlas[rect.x + x, rect.y + y] == 0, "Atlas texture packing encountered an overlap error"
+        result.atlas[rect.x + x, rect.y + y] = images[rect.i][x, y]
+        result.coords[rect.i] = (x: rect.x, y: rect.y)