Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spritesheet Autoslicer #893

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
21596f8
Added autoSlicer
Variable-ind Aug 1, 2023
422a06c
Fixed the remaining things (Autoslicer fully functional)
Variable-ind Aug 2, 2023
6600b73
Update RegionUnpacker.gd
Variable-ind Aug 2, 2023
9b515ac
Formatting
Variable-ind Aug 2, 2023
e5afc15
formatting
Variable-ind Aug 2, 2023
25633e5
formatting
Variable-ind Aug 2, 2023
c1e2f32
neatify
Variable-ind Aug 2, 2023
65a9bad
Update RegionUnpacker.gd
Variable-ind Aug 2, 2023
4285106
formatting
Variable-ind Aug 2, 2023
85e7a7c
formatting
Variable-ind Aug 2, 2023
57803cd
Update RegionUnpacker.gd
Variable-ind Aug 2, 2023
fc9c4fe
Update README.md
Variable-ind Aug 2, 2023
67e21ac
Added region unpacker class
Variable-ind Aug 3, 2023
add1490
Optimized the region unpacker, this should now be twice as fast
Variable-ind Aug 3, 2023
ee7919f
change Smart Slicer to 5745b26a6e0b7e10bc4a46d07b5f9f0dd8f26c96
Variable-ind Aug 3, 2023
26434fe
Delete addons/SmartSlicer/Shader directory
Variable-ind Aug 3, 2023
08067d6
removed shader
Variable-ind Aug 3, 2023
758c889
Update SmartSlicer Version
Variable-ind Aug 3, 2023
234d9cc
Formatting (This is torture LOL)
Variable-ind Aug 3, 2023
4381ff8
Delete addons/SmartSlicer/addons/SmartSlicer/Classes directory
Variable-ind Aug 3, 2023
840d2f9
Formatting
Variable-ind Aug 3, 2023
9caf771
Delete SmartSlicePreview.gd
Variable-ind Aug 3, 2023
34020ca
use _draw instead of line2d
Variable-ind Aug 3, 2023
9506de7
Formatting
Variable-ind Aug 3, 2023
066ec0f
More formatting
Variable-ind Aug 3, 2023
06bdd10
Fix bugs related to import
Variable-ind Aug 3, 2023
8dfea1e
fix crash on attempting to open empty image
Variable-ind Aug 5, 2023
4c53779
removed accidental print
Variable-ind Aug 5, 2023
dcacd3f
fix empty image warnings
Variable-ind Aug 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions addons/README.md
Expand Up @@ -27,3 +27,10 @@ Files extracted from source:
- Version: Based on git commit e5df60ed1d53246e03dba36053ff009846ba5174 with a modification on dockable_container.gd (lines 187-191).
- License: [CC0-1.0](https://github.com/gilzoide/godot-dockable-container/blob/main/LICENSE)

## SmartSlicer

- Upstream: https://github.com/Variable-Interactive/SmartSlicer
- Version: Based on git commit 2804e6109f9667022c66522ce88a99a56fd67ca8 with a modification on SmartSlicePreview.gd (lines 31-32). Only the contents of addons folder are used and the script SmartSlicePreview.gd is moved to res://src/UI/Dialogs/HelperScripts/ for better organization
- License: [MIT](https://github.com/Variable-Interactive/SmartSlicer/blob/main/LICENSE)


251 changes: 251 additions & 0 deletions addons/SmartSlicer/Classes/RegionUnpacker.gd
@@ -0,0 +1,251 @@
class_name RegionUnpacker
extends Reference

# THIS CLASS TAKES INSPIRATION FROM PIXELORAMA'S FLOOD FILL
# AND HAS BEEN MODIFIED FOR OPTIMIZATION

var slice_thread := Thread.new()

var _include_boundary_threshold: int # the size of rect below which merging accounts for boundaty
var _merge_dist: int # after crossing threshold the smaller image will merge with larger image
# if it is within the _merge_dist

# working array used as buffer for segments while flooding
var _allegro_flood_segments: Array
# results array per image while flooding
var _allegro_image_segments: Array


func _init(threshold: int, merge_dist: int) -> void:
_include_boundary_threshold = threshold
_merge_dist = merge_dist


func get_used_rects(image: Image) -> Dictionary:
if OS.get_name() == "HTML5":
return get_rects(image)
else:
# If Thread model is set to "Multi-Threaded" in project settings>threads>thread model
if slice_thread.is_active():
slice_thread.wait_to_finish()
var error = slice_thread.start(self, "get_rects", image)
if error == OK:
return slice_thread.wait_to_finish()
else:
return get_rects(image)
# If Thread model is set to "Single-Safe" in project settings>threads>thread model then
# comment the above code and uncomment below
#return get_rects({"image": image})


func get_rects(image: Image) -> Dictionary:
# make a smaller image to make the loop shorter
var used_rect = image.get_used_rect()
if used_rect.size == Vector2.ZERO:
return clean_rects([])
var test_image = image.get_rect(used_rect)
# prepare a bitmap to keep track of previous places
var scanned_area := BitMap.new()
scanned_area.create(test_image.get_size())
test_image.lock()
# Scan the image
var rects = []
var frame_size = Vector2.ZERO
for y in test_image.get_size().y:
for x in test_image.get_size().x:
var position = Vector2(x, y)
if test_image.get_pixelv(position).a > 0: # used portion of image detected
if !scanned_area.get_bit(position):
var rect := _estimate_rect(test_image, position)
scanned_area.set_bit_rect(rect, true)
rect.position += used_rect.position
rects.append(rect)
test_image.unlock()
var rects_info = clean_rects(rects)
rects_info["rects"].sort_custom(self, "sort_rects")
return rects_info


func clean_rects(rects: Array) -> Dictionary:
var frame_size = Vector2.ZERO
for i in rects.size():
var target: Rect2 = rects.pop_front()
var test_rect = target
if (
target.size.x < _include_boundary_threshold
or target.size.y < _include_boundary_threshold
):
test_rect.size += Vector2(_merge_dist, _merge_dist)
test_rect.position -= Vector2(_merge_dist, _merge_dist) / 2
var merged = false
for rect_i in rects.size():
if test_rect.intersects(rects[rect_i]):
rects[rect_i] = target.merge(rects[rect_i])
merged = true
break
if !merged:
rects.append(target)

# calculation for a suitable frame size
if target.size.x > frame_size.x:
frame_size.x = target.size.x
if target.size.y > frame_size.y:
frame_size.y = target.size.y
return {"rects": rects, "frame_size": frame_size}


func sort_rects(rect_a: Rect2, rect_b: Rect2) -> bool:
# After many failed attempts, this version works for some reason (it's best not to disturb it)
if rect_a.end.y < rect_b.position.y:
return true
if rect_a.position.x < rect_b.position.x:
# if both lie in the same row
var start = rect_a.position
var size = Vector2(rect_b.end.x, rect_a.end.y)
if Rect2(start, size).intersects(rect_b):
return true
return false


func _estimate_rect(image: Image, position: Vector2) -> Rect2:
var cel_image := Image.new()
cel_image.copy_from(image)
cel_image.lock()
var small_rect: Rect2 = _flood_fill(position, cel_image)
cel_image.unlock()
return small_rect


# Add a new segment to the array
func _add_new_segment(y: int = 0) -> void:
var segment = {}
segment.flooding = false
segment.todo_above = false
segment.todo_below = false
segment.left_position = -5 # anything less than -1 is ok
segment.right_position = -5
segment.y = y
segment.next = 0
_allegro_flood_segments.append(segment)


# fill an horizontal segment around the specified position, and adds it to the
# list of segments filled. Returns the first x coordinate after the part of the
# line that has been filled.
func _flood_line_around_point(position: Vector2, image: Image) -> int:
# this method is called by `_flood_fill` after the required data structures
# have been initialized
if not image.get_pixelv(position).a > 0:
return int(position.x) + 1
var west: Vector2 = position
var east: Vector2 = position
while west.x >= 0 && image.get_pixelv(west).a > 0:
west += Vector2.LEFT
while east.x < image.get_width() && image.get_pixelv(east).a > 0:
east += Vector2.RIGHT
# Make a note of the stuff we processed
var c = int(position.y)
var segment = _allegro_flood_segments[c]
# we may have already processed some segments on this y coordinate
if segment.flooding:
while segment.next > 0:
c = segment.next # index of next segment in this line of image
segment = _allegro_flood_segments[c]
# found last current segment on this line
c = _allegro_flood_segments.size()
segment.next = c
_add_new_segment(int(position.y))
segment = _allegro_flood_segments[c]
# set the values for the current segment
segment.flooding = true
segment.left_position = west.x + 1
segment.right_position = east.x - 1
segment.y = position.y
segment.next = 0
# Should we process segments above or below this one?
# when there is a selected area, the pixels above and below the one we started creating this
# segment from may be outside it. It's easier to assume we should be checking for segments
# above and below this one than to specifically check every single pixel in it, because that
# test will be performed later anyway.
# On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if
# there is no selection, so we don't need branching here.
segment.todo_above = position.y > 0
segment.todo_below = position.y < image.get_height() - 1
# this is an actual segment we should be coloring, so we add it to the results for the
# current image
if segment.right_position >= segment.left_position:
_allegro_image_segments.append(segment)
# we know the point just east of the segment is not part of a segment that should be
# processed, else it would be part of this segment
return int(east.x) + 1


func _check_flooded_segment(y: int, left: int, right: int, image: Image) -> bool:
var ret = false
var c: int = 0
while left <= right:
c = y
while true:
var segment = _allegro_flood_segments[c]
if left >= segment.left_position and left <= segment.right_position:
left = segment.right_position + 2
break
c = segment.next
if c == 0: # couldn't find a valid segment, so we draw a new one
left = _flood_line_around_point(Vector2(left, y), image)
ret = true
break
return ret


func _flood_fill(position: Vector2, image: Image) -> Rect2:
# implements the floodfill routine by Shawn Hargreaves
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
# init flood data structures
_allegro_flood_segments = []
_allegro_image_segments = []
_compute_segments_for_image(position, image)
# now actually color the image: since we have already checked a few things for the points
# we'll process here, we're going to skip a bunch of safety checks to speed things up.

var final_image = Image.new()
final_image.copy_from(image)
final_image.fill(Color.transparent)
final_image.lock()
_select_segments(final_image)
final_image.unlock()

return final_image.get_used_rect()


func _compute_segments_for_image(position: Vector2, image: Image) -> void:
# initially allocate at least 1 segment per line of image
for j in image.get_height():
_add_new_segment(j)
# start flood algorithm
_flood_line_around_point(position, image)
# test all segments while also discovering more
var done := false
while not done:
done = true
var max_index = _allegro_flood_segments.size()
for c in max_index:
var p = _allegro_flood_segments[c]
if p.todo_below: # check below the segment?
p.todo_below = false
if _check_flooded_segment(p.y + 1, p.left_position, p.right_position, image):
done = false
if p.todo_above: # check above the segment?
p.todo_above = false
if _check_flooded_segment(p.y - 1, p.left_position, p.right_position, image):
done = false


func _select_segments(map: Image) -> void:
# short circuit for flat colors
for c in _allegro_image_segments.size():
var p = _allegro_image_segments[c]
var rect = Rect2()
rect.position = Vector2(p.left_position, p.y)
rect.end = Vector2(p.right_position + 1, p.y + 1)
map.fill_rect(rect, Color.white)
6 changes: 6 additions & 0 deletions project.godot
Expand Up @@ -224,6 +224,11 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://src/UI/ReferenceImages/ReferencesPanel.gd"
}, {
"base": "Reference",
"class": "RegionUnpacker",
"language": "GDScript",
"path": "res://addons/SmartSlicer/Classes/RegionUnpacker.gd"
}, {
"base": "Image",
"class": "SelectionMap",
"language": "GDScript",
Expand Down Expand Up @@ -313,6 +318,7 @@ _global_script_class_icons={
"Project": "",
"ReferenceImage": "",
"ReferencesPanel": "",
"RegionUnpacker": "",
"SelectionMap": "",
"SelectionTool": "",
"ShaderImageEffect": "",
Expand Down