From b1185da40341bf1fb6deda37449857facc48ee6e Mon Sep 17 00:00:00 2001 From: Chris Blackbourn Date: Wed, 8 Mar 2023 20:33:13 +1300 Subject: [PATCH] Fix #102843: Add UV Packer with O(nlogn) performance Adds a novel "Alpaca" UV packing strategy for fast packing of UV island AABBs without rotation. Pull Request: https://projects.blender.org/blender/blender/pulls/105393 --- source/blender/geometry/intern/uv_pack.cc | 161 ++++++++++++++++++++-- 1 file changed, 150 insertions(+), 11 deletions(-) diff --git a/source/blender/geometry/intern/uv_pack.cc b/source/blender/geometry/intern/uv_pack.cc index 7768770b1c6..9c754639f55 100644 --- a/source/blender/geometry/intern/uv_pack.cc +++ b/source/blender/geometry/intern/uv_pack.cc @@ -6,11 +6,14 @@ #include "GEO_uv_pack.hh" +#include "BLI_array.hh" #include "BLI_boxpack_2d.h" #include "BLI_convexhull_2d.h" #include "BLI_listbase.h" #include "BLI_math.h" +#include "BLI_math_matrix.hh" #include "BLI_rect.h" +#include "BLI_vector.hh" #include "DNA_meshdata_types.h" #include "DNA_scene_types.h" @@ -20,21 +23,157 @@ namespace blender::geometry { -static float pack_islands_scale_margin(const Span &island_vector, +/* Compact representation for AABB packers. */ +class UVAABBIsland { + public: + float2 uv_diagonal; + float2 uv_placement; + int64_t index; +}; + +/** + * Pack AABB islands using the "Alpaca" strategy, with no rotation. + * + * Each box is packed into an "L" shaped region, gradually filling up space. + * "Alpaca" is a pun, as it's pronounced the same as "L-Packer" in English. + * + * In theory, alpaca_turbo should be the fastest non-trivial packer, hence the "turbo" suffix. + * + * Technically, the algorithm here is only O(n), In practice, to get reasonable results, the input + * must be pre-sorted, which costs an additional O(nlogn) time complexity. + */ +static void pack_islands_alpaca_turbo(const Span islands, + float *r_max_u, + float *r_max_v) +{ + /* Exclude an initial AABB near the origin. */ + float next_u1 = *r_max_u; + float next_v1 = *r_max_v; + bool zigzag = next_u1 < next_v1; /* Horizontal or Vertical strip? */ + + float u0 = zigzag ? next_u1 : 0.0f; + float v0 = zigzag ? 0.0f : next_v1; + + /* Visit every island in order. */ + for (UVAABBIsland *island : islands) { + float dsm_u = island->uv_diagonal.x; + float dsm_v = island->uv_diagonal.y; + + bool restart = false; + if (zigzag) { + restart = (next_v1 < v0 + dsm_v); + } + else { + restart = (next_u1 < u0 + dsm_u); + } + if (restart) { + /* We're at the end of a strip. Restart from U axis or V axis. */ + zigzag = next_u1 < next_v1; + u0 = zigzag ? next_u1 : 0.0f; + v0 = zigzag ? 0.0f : next_v1; + } + + /* Place the island. */ + island->uv_placement.x = u0; + island->uv_placement.y = v0; + if (zigzag) { + /* Move upwards. */ + v0 += dsm_v; + next_u1 = max_ff(next_u1, u0 + dsm_u); + next_v1 = max_ff(next_v1, v0); + } + else { + /* Move sideways. */ + u0 += dsm_u; + next_v1 = max_ff(next_v1, v0 + dsm_v); + next_u1 = max_ff(next_u1, u0); + } + } + + /* Write back total pack AABB. */ + *r_max_u = next_u1; + *r_max_v = next_v1; +} + +static float pack_islands_scale_margin(const Span islands, BoxPack *box_array, const float scale, const float margin) { - for (const int64_t index : island_vector.index_range()) { - PackIsland *island = island_vector[index]; - BoxPack *box = &box_array[index]; - box->index = int(index); - box->w = BLI_rctf_size_x(&island->bounds_rect) * scale + 2 * margin; - box->h = BLI_rctf_size_y(&island->bounds_rect) * scale + 2 * margin; + /* #BLI_box_pack_2d produces layouts with high packing efficiency, but has O(n^3) + * time complexity, causing poor performance if there are lots of islands. See: #102843. + * #pack_islands_alpaca_turbo is designed to be the fastest packing method, O(nlogn), + * but has poor packing efficiency if the AABBs have a spread of sizes and aspect ratios. + * Here, we merge the best properties of both packers into one combined packer. + + * The free tuning parameter, `alpaca_cutoff` will determine how many islands are packed + * using each method. + + * The current strategy is: + * - Sort islands in size order. + * - Call #BLI_box_pack_2d on the first `alpaca_cutoff` islands. + * - Call #pack_islands_alpaca_turbo on the remaining islands. + * - Combine results. + + */ + + /* First, copy information from our input into the AABB structure. */ + Array aabbs(islands.size()); + for (const int64_t i : islands.index_range()) { + PackIsland *pack_island = islands[i]; + UVAABBIsland *aabb = new UVAABBIsland(); + aabb->index = i; + aabb->uv_diagonal.x = BLI_rctf_size_x(&pack_island->bounds_rect) * scale + 2 * margin; + aabb->uv_diagonal.y = BLI_rctf_size_y(&pack_island->bounds_rect) * scale + 2 * margin; + aabbs[i] = aabb; } - float max_u, max_v; - BLI_box_pack_2d(box_array, int(island_vector.size()), &max_u, &max_v); - return max_ff(max_u, max_v); + + /* Sort from "biggest" to "smallest". */ + std::stable_sort(aabbs.begin(), aabbs.end(), [](const UVAABBIsland *a, const UVAABBIsland *b) { + /* Just choose the AABB with larger rectangular area. */ + return b->uv_diagonal.x * b->uv_diagonal.y < a->uv_diagonal.x * a->uv_diagonal.y; + }); + + /* Partition island_vector, largest will go to box_pack, the rest alpaca_turbo. + * See discussion above for details. */ + const int64_t alpaca_cutoff = int64_t(1024); /* TODO: Tune constant. */ + int64_t max_box_pack = std::min(alpaca_cutoff, islands.size()); + + /* Prepare for box_pack_2d. */ + for (const int64_t i : islands.index_range()) { + UVAABBIsland *aabb = aabbs[i]; + BoxPack *box = &box_array[i]; + box->index = int(aabb->index); + box->w = aabb->uv_diagonal.x; + box->h = aabb->uv_diagonal.y; + } + + /* Call box_pack_2d (slow for large N.) */ + float max_u = 0.0f; + float max_v = 0.0f; + BLI_box_pack_2d(box_array, int(max_box_pack), &max_u, &max_v); + + /* At this stage, `max_u` and `max_v` contain the box_pack UVs. */ + + /* Call Alpaca. */ + pack_islands_alpaca_turbo(aabbs.as_span().drop_front(max_box_pack), &max_u, &max_v); + + /* Write back Alpaca UVs. */ + for (int64_t index = max_box_pack; index < aabbs.size(); index++) { + UVAABBIsland *aabb = aabbs[index]; + BoxPack *box = &box_array[index]; + box->x = aabb->uv_placement.x; + box->y = aabb->uv_placement.y; + } + + /* Memory management. */ + for (int64_t i : aabbs.index_range()) { + UVAABBIsland *aabb = aabbs[i]; + aabbs[i] = nullptr; + delete aabb; + } + + return std::max(max_u, max_v); } static float pack_islands_margin_fraction(const Span &island_vector, @@ -102,7 +241,7 @@ static float pack_islands_margin_fraction(const Span &island_vecto } } - scale = max_ff(scale, min_scale_roundoff); + scale = std::max(scale, min_scale_roundoff); /* Evaluate our `f`. */ scale_last = scale;