From ab7485194dfa461614bbc714f48ca2b69e079f95 Mon Sep 17 00:00:00 2001 From: Chris Blackbourn Date: Wed, 22 Mar 2023 15:46:45 +1300 Subject: [PATCH 1/2] UV: Add alpaca_rotate variant for improved packing efficiency --- .../editors/uvedit/uvedit_unwrap_ops.cc | 12 +- source/blender/geometry/GEO_uv_pack.hh | 3 + source/blender/geometry/intern/uv_pack.cc | 210 ++++++++++++++++-- .../geometry/intern/uv_parametrizer.cc | 16 +- 4 files changed, 210 insertions(+), 31 deletions(-) diff --git a/source/blender/editors/uvedit/uvedit_unwrap_ops.cc b/source/blender/editors/uvedit/uvedit_unwrap_ops.cc index 5cad7255da3..46cfc5743cd 100644 --- a/source/blender/editors/uvedit/uvedit_unwrap_ops.cc +++ b/source/blender/editors/uvedit/uvedit_unwrap_ops.cc @@ -1407,6 +1407,8 @@ static void uvedit_pack_islands_multi(const Scene *scene, blender::geometry::PackIsland *pack_island = new blender::geometry::PackIsland(); pack_island->bounds_rect = face_island->bounds_rect; pack_island->caller_index = i; + pack_island->aspect_y = face_island->aspect_y; + pack_island->angle = 0.0f; pack_island_vector.append(pack_island); for (int i = 0; i < face_island->faces_len; i++) { @@ -1473,10 +1475,12 @@ static void uvedit_pack_islands_multi(const Scene *scene, for (int64_t i : pack_island_vector.index_range()) { blender::geometry::PackIsland *pack_island = pack_island_vector[i]; FaceIsland *island = island_vector[pack_island->caller_index]; - matrix[0][0] = scale[0]; - matrix[0][1] = 0.0f; - matrix[1][0] = 0.0f; - matrix[1][1] = scale[1]; + const float cos_angle = cosf(pack_island->angle); + const float sin_angle = sinf(pack_island->angle); + matrix[0][0] = cos_angle * scale[0]; + matrix[0][1] = -sin_angle * scale[0] * pack_island->aspect_y; + matrix[1][0] = sin_angle * scale[1] / pack_island->aspect_y; + matrix[1][1] = cos_angle * scale[1]; invert_m2_m2(matrix_inverse, matrix); /* Add base_offset, post transform. */ diff --git a/source/blender/geometry/GEO_uv_pack.hh b/source/blender/geometry/GEO_uv_pack.hh index f53f81ece7c..d52cdd86704 100644 --- a/source/blender/geometry/GEO_uv_pack.hh +++ b/source/blender/geometry/GEO_uv_pack.hh @@ -73,8 +73,11 @@ class UVPackIsland_Params { class PackIsland { public: rctf bounds_rect; + /** Aspect ratio, required for rotation. */ + float aspect_y; /** Output. */ float2 pre_translate; + float angle; /** Unchanged by #pack_islands, used by caller. */ int caller_index; diff --git a/source/blender/geometry/intern/uv_pack.cc b/source/blender/geometry/intern/uv_pack.cc index da9396634e9..54db292868a 100644 --- a/source/blender/geometry/intern/uv_pack.cc +++ b/source/blender/geometry/intern/uv_pack.cc @@ -180,6 +180,7 @@ class UVAABBIsland { float2 uv_diagonal; float2 uv_placement; int64_t index; + float angle; }; /** @@ -508,7 +509,7 @@ static void pack_island_xatlas(const Span island_indices, else { /* Increasing by 2 here has the effect of changing the sampling pattern. * The parameter '2' is not "free" in the sense that changing it requires - * a change to `bitmap_radix` and then returning `alpaca_cutoff`. + * a change to `bitmap_radix` and then re-tuning `alpaca_cutoff`. * Possible values here *could* be 1, 2 or 3, however the only *reasonable* * choice is 2. */ scan_line += 2; @@ -551,6 +552,132 @@ static void pack_island_xatlas(const Span island_indices, *r_max_v = max_v; } +static void update_hole_rotate(float2 &hole, + float2 &hole_diagonal, + bool &hole_rotate, + const float u0, + const float v0, + const float u1, + const float v1) +{ + const float p = hole_diagonal.x * hole_diagonal.y; + const float quad_area = (u1 - u0) * (v1 - v0); + if (p >= quad_area) { + return; + } + hole.x = u0; + hole.y = v0; + hole_diagonal.x = u1 - u0; + hole_diagonal.y = v1 - v0; + if (hole_diagonal.y < hole_diagonal.x) { + std::swap(hole_diagonal.x, hole_diagonal.y); + hole_rotate = true; + } + else { + hole_rotate = false; + } + const float q = hole_diagonal.x * hole_diagonal.y; + BLI_assert(q > p); + UNUSED_VARS(q); +} + +/** + * Pack AABB islands using the "Alpaca" strategy, with rotation. + * + * Same as #pack_islands_alpaca_turbo with support for rotation in 90 degree increments. + */ +static void pack_islands_alpaca_rotate(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? */ + + /* Track an AABB "hole" which may be filled at any time. */ + float2 hole(0.0f); + float2 hole_diagonal(0.0f); + bool hole_rotate = false; + + float u0 = zigzag ? next_u1 : 0.0f; + float v0 = zigzag ? 0.0f : next_v1; + + /* Visit every island in order. */ + for (UVAABBIsland *island : islands) { + float min_dsm = std::min(island->uv_diagonal.x, island->uv_diagonal.y); + float max_dsm = std::max(island->uv_diagonal.x, island->uv_diagonal.y); + + if (min_dsm < hole_diagonal.x && max_dsm < hole_diagonal.y) { + /* Place island in the hole. */ + island->uv_placement.x = hole[0]; + island->uv_placement.y = hole[1]; + if (hole_rotate == (min_dsm == island->uv_diagonal.x)) { + island->angle = DEG2RADF(90.0f); + } + else { + island->angle = 0.0f; + } + + /* Update space left in the hole. */ + float p[6]; + p[0] = hole[0]; + p[1] = hole[1]; + p[2] = hole[0] + (hole_rotate ? max_dsm : min_dsm); + p[3] = hole[1] + (hole_rotate ? min_dsm : max_dsm); + p[4] = hole[0] + (hole_rotate ? hole_diagonal.y : hole_diagonal.x); + p[5] = hole[1] + (hole_rotate ? hole_diagonal.x : hole_diagonal.y); + hole_diagonal.x = 0; /* Invalidate old hole. */ + update_hole_rotate(hole, hole_diagonal, hole_rotate, p[0], p[3], p[4], p[5]); + update_hole_rotate(hole, hole_diagonal, hole_rotate, p[2], p[1], p[4], p[5]); + + /* Island is placed, no need to check for restart. */ + continue; + } + + bool restart = false; + if (zigzag) { + restart = (next_v1 < v0 + min_dsm); + } + else { + restart = (next_u1 < u0 + min_dsm); + } + if (restart) { + update_hole_rotate(hole, hole_diagonal, hole_rotate, u0, v0, next_u1, next_v1); + /* 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. */ + if (zigzag == (min_dsm == island->uv_diagonal.x)) { + island->angle = DEG2RADF(90.0f); + } + else { + island->angle = 0.0f; + } + island->uv_placement.x = u0; + island->uv_placement.y = v0; + if (zigzag) { + /* Move upwards. */ + v0 += min_dsm; + next_u1 = max_ff(next_u1, u0 + max_dsm); + next_v1 = max_ff(next_v1, v0); + } + else { + /* Move sideways. */ + u0 += min_dsm; + next_v1 = max_ff(next_v1, v0 + max_dsm); + 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, @@ -569,7 +696,7 @@ static float pack_islands_scale_margin(const Span islands, * 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. + * - Call #pack_islands_alpaca_* on the remaining islands. * - Combine results. */ @@ -585,10 +712,36 @@ static float pack_islands_scale_margin(const Span islands, } /* 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; - }); + + if (params.rotate) { + std::stable_sort(aabbs.begin(), aabbs.end(), [](const UVAABBIsland *a, const UVAABBIsland *b) { + /* Choose the AABB with the longest large edge. */ + float a_u = a->uv_diagonal.x; + float a_v = a->uv_diagonal.y; + float b_u = b->uv_diagonal.x; + float b_v = b->uv_diagonal.y; + if (a_u > a_v) { + std::swap(a_u, a_v); + } + if (b_u > b_v) { + std::swap(b_u, b_v); + } + float diff_u = a_u - b_u; + float diff_v = a_v - b_v; + diff_v += diff_u * 0.05f; /* Robust sort, smooth over round-off errors. */ + if (diff_v == 0.0f) { /* Tie break. */ + return diff_u > 0.0f; + } + return diff_v > 0.0f; + }); + } + else { + + std::stable_sort(aabbs.begin(), aabbs.end(), [](const UVAABBIsland *a, const UVAABBIsland *b) { + /* 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 `islands`, largest will go to a slow packer, the rest alpaca_turbo. * See discussion above for details. */ @@ -632,14 +785,22 @@ static float pack_islands_scale_margin(const Span islands, /* 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); + if (params.rotate) { + pack_islands_alpaca_rotate(aabbs.as_mutable_span().drop_front(max_box_pack), &max_u, &max_v); + } + else { + pack_islands_alpaca_turbo(aabbs.as_mutable_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]; + for (int64_t i = max_box_pack; i < aabbs.size(); i++) { + UVAABBIsland *aabb = aabbs[i]; + BoxPack *box = &box_array[i]; box->x = aabb->uv_placement.x; box->y = aabb->uv_placement.y; + + PackIsland *pack_island = islands[aabb->index]; + pack_island->angle = aabb->angle; } /* Memory management. */ @@ -748,6 +909,7 @@ static float pack_islands_margin_fraction(const Span &island_vecto scale_last = scale_low; const float max_uv = pack_islands_scale_margin( island_vector, box_array, scale_last, margin_fraction, params); + BLI_assert(max_uv == value_low); UNUSED_VARS(max_uv); /* TODO (?): `if (max_uv < 1.0f) { scale_last /= max_uv; }` */ } @@ -780,16 +942,16 @@ static float calc_margin_from_aabb_length_sum(const Span &island_v return params.margin * aabb_length_sum * 0.1f; } -static BoxPack *pack_islands_box_array(const Span &island_vector, +static BoxPack *pack_islands_box_array(const Span &islands, const UVPackIsland_Params ¶ms, float r_scale[2]) { BoxPack *box_array = static_cast( - MEM_mallocN(sizeof(*box_array) * island_vector.size(), __func__)); + MEM_mallocN(sizeof(*box_array) * islands.size(), __func__)); if (params.margin == 0.0f) { /* Special case for zero margin. Margin_method is ignored as all formulas give same result. */ - const float max_uv = pack_islands_scale_margin(island_vector, box_array, 1.0f, 0.0f, params); + const float max_uv = pack_islands_scale_margin(islands, box_array, 1.0f, 0.0f, params); r_scale[0] = 1.0f / max_uv; r_scale[1] = r_scale[0]; return box_array; @@ -797,8 +959,7 @@ static BoxPack *pack_islands_box_array(const Span &island_vector, if (params.margin_method == ED_UVPACK_MARGIN_FRACTION) { /* Uses a line search on scale. ~10x slower than other method. */ - const float scale = pack_islands_margin_fraction( - island_vector, box_array, params.margin, params); + const float scale = pack_islands_margin_fraction(islands, box_array, params.margin, params); r_scale[0] = scale; r_scale[1] = scale; /* pack_islands_margin_fraction will pad FaceIslands, return early. */ @@ -810,7 +971,7 @@ static BoxPack *pack_islands_box_array(const Span &island_vector, case ED_UVPACK_MARGIN_ADD: /* Default for Blender 2.8 and earlier. */ break; /* Nothing to do. */ case ED_UVPACK_MARGIN_SCALED: /* Default for Blender 3.3 and later. */ - margin = calc_margin_from_aabb_length_sum(island_vector, params); + margin = calc_margin_from_aabb_length_sum(islands, params); break; case ED_UVPACK_MARGIN_FRACTION: /* Added as an option in Blender 3.4. */ BLI_assert_unreachable(); /* Handled above. */ @@ -819,12 +980,12 @@ static BoxPack *pack_islands_box_array(const Span &island_vector, BLI_assert_unreachable(); } - const float max_uv = pack_islands_scale_margin(island_vector, box_array, 1.0f, margin, params); + const float max_uv = pack_islands_scale_margin(islands, box_array, 1.0f, margin, params); r_scale[0] = 1.0f / max_uv; r_scale[1] = r_scale[0]; - for (int index = 0; index < island_vector.size(); index++) { - PackIsland *island = island_vector[index]; + for (int index = 0; index < islands.size(); index++) { + PackIsland *island = islands[index]; BLI_rctf_pad(&island->bounds_rect, margin, margin); } return box_array; @@ -839,8 +1000,15 @@ void pack_islands(const Span &islands, for (int64_t i : islands.index_range()) { BoxPack *box = box_array + i; PackIsland *island = islands[box->index]; - island->pre_translate.x = box->x - island->bounds_rect.xmin; - island->pre_translate.y = box->y - island->bounds_rect.ymin; + if (island->angle) { + /* TODO: Apply proper rotation. */ + island->pre_translate.x = (-box->y / island->aspect_y) - island->bounds_rect.xmax; + island->pre_translate.y = (box->x * island->aspect_y) - island->bounds_rect.ymin; + } + else { + island->pre_translate.x = box->x - island->bounds_rect.xmin; + island->pre_translate.y = box->y - island->bounds_rect.ymin; + } } MEM_freeN(box_array); diff --git a/source/blender/geometry/intern/uv_parametrizer.cc b/source/blender/geometry/intern/uv_parametrizer.cc index 200150f354d..8ea59361e20 100644 --- a/source/blender/geometry/intern/uv_parametrizer.cc +++ b/source/blender/geometry/intern/uv_parametrizer.cc @@ -4168,6 +4168,8 @@ void uv_parametrizer_pack(ParamHandle *handle, float margin, bool do_rotate, boo geometry::PackIsland *pack_island = new geometry::PackIsland(); pack_island->caller_index = i; + pack_island->aspect_y = handle->aspx / handle->aspy; + pack_island->angle = 0.0f; pack_island_vector.append(pack_island); float minv[2]; @@ -4191,16 +4193,18 @@ void uv_parametrizer_pack(ParamHandle *handle, float margin, bool do_rotate, boo PackIsland *pack_island = pack_island_vector[i]; PChart *chart = handle->charts[pack_island->caller_index]; - float m[2][2]; + float matrix[2][2]; float b[2]; - m[0][0] = scale[0]; - m[0][1] = 0.0f; - m[1][0] = 0.0f; - m[1][1] = scale[1]; + const float cos_angle = cosf(pack_island->angle); + const float sin_angle = sinf(pack_island->angle); + matrix[0][0] = cos_angle * scale[0]; + matrix[0][1] = -sin_angle * scale[0] * pack_island->aspect_y; + matrix[1][0] = sin_angle * scale[1] / pack_island->aspect_y; + matrix[1][1] = cos_angle * scale[1]; b[0] = pack_island->pre_translate.x; b[1] = pack_island->pre_translate.y; for (PVert *v = chart->verts; v; v = v->nextlink) { - blender::geometry::mul_v2_m2_add_v2v2(v->uv, m, v->uv, b); + blender::geometry::mul_v2_m2_add_v2v2(v->uv, matrix, v->uv, b); } pack_island_vector[i] = nullptr; -- 2.30.2 From 8239df345ee02a5e23ef49dfdf1955f92c504049 Mon Sep 17 00:00:00 2001 From: Chris Blackbourn Date: Tue, 28 Mar 2023 11:10:27 +1300 Subject: [PATCH 2/2] Improve comments. --- source/blender/geometry/GEO_uv_pack.hh | 4 +- source/blender/geometry/intern/uv_pack.cc | 50 +++++++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/source/blender/geometry/GEO_uv_pack.hh b/source/blender/geometry/GEO_uv_pack.hh index d52cdd86704..2c5e7f507c2 100644 --- a/source/blender/geometry/GEO_uv_pack.hh +++ b/source/blender/geometry/GEO_uv_pack.hh @@ -72,11 +72,13 @@ class UVPackIsland_Params { class PackIsland { public: + /** Bounding rectangle of input. Will be calculated automatically in a future update. */ rctf bounds_rect; /** Aspect ratio, required for rotation. */ float aspect_y; - /** Output. */ + /** Output pre-translation. */ float2 pre_translate; + /** Output angle in radians. */ float angle; /** Unchanged by #pack_islands, used by caller. */ int caller_index; diff --git a/source/blender/geometry/intern/uv_pack.cc b/source/blender/geometry/intern/uv_pack.cc index 54db292868a..f1a2e1f8fc2 100644 --- a/source/blender/geometry/intern/uv_pack.cc +++ b/source/blender/geometry/intern/uv_pack.cc @@ -552,6 +552,18 @@ static void pack_island_xatlas(const Span island_indices, *r_max_v = max_v; } +/** + * Helper function for #pack_islands_alpaca_rotate + * + * The "Hole" is an AABB region of the UV plane that is stored in an unusual way. + * `hole` is the xy position of lower left corner of the AABB. + * `hole_diagonal` is the extent of the AABB, possibly flipped. + * `hole_rotate` is a boolean value, tracking if `hole_diagonal` is flipped. + * + * Given an alternate AABB specified by `(u0, v0, u1, v1)`, the helper will + * update the Hole to the condidate location if it is larger. + */ + static void update_hole_rotate(float2 &hole, float2 &hole_diagonal, bool &hole_rotate, @@ -560,10 +572,12 @@ static void update_hole_rotate(float2 &hole, const float u1, const float v1) { - const float p = hole_diagonal.x * hole_diagonal.y; + BLI_assert(hole_diagonal.x <= hole_diagonal.y); /* Confirm invariants. */ + + const float hole_area = hole_diagonal.x * hole_diagonal.y; const float quad_area = (u1 - u0) * (v1 - v0); - if (p >= quad_area) { - return; + if (quad_area <= hole_area) { + return; /* No update, existing hole is larger than candidate. */ } hole.x = u0; hole.y = v0; @@ -576,15 +590,21 @@ static void update_hole_rotate(float2 &hole, else { hole_rotate = false; } - const float q = hole_diagonal.x * hole_diagonal.y; - BLI_assert(q > p); - UNUSED_VARS(q); + + const float updated_area = hole_diagonal.x * hole_diagonal.y; + BLI_assert(hole_area < updated_area); /* Confirm hole grew in size. */ + UNUSED_VARS(updated_area); + + BLI_assert(hole_diagonal.x <= hole_diagonal.y); /* Confirm invariants. */ } /** * Pack AABB islands using the "Alpaca" strategy, with rotation. * - * Same as #pack_islands_alpaca_turbo with support for rotation in 90 degree increments. + * Same as #pack_islands_alpaca_turbo, with support for rotation in 90 degree increments. + * + * Also adds the concept of a "Hole", which is unused space that can be filled. + * Tracking the "Hole" has a slight performance cost, while improving packing efficiency. */ static void pack_islands_alpaca_rotate(const Span islands, float *r_max_u, @@ -631,7 +651,7 @@ static void pack_islands_alpaca_rotate(const Span islands, update_hole_rotate(hole, hole_diagonal, hole_rotate, p[0], p[3], p[4], p[5]); update_hole_rotate(hole, hole_diagonal, hole_rotate, p[2], p[1], p[4], p[5]); - /* Island is placed, no need to check for restart. */ + /* Island is placed in the hole, no need to check for restart, or process movement. */ continue; } @@ -659,6 +679,8 @@ static void pack_islands_alpaca_rotate(const Span islands, } island->uv_placement.x = u0; island->uv_placement.y = v0; + + /* Move according to the "Alpaca rules", with rotation. */ if (zigzag) { /* Move upwards. */ v0 += min_dsm; @@ -700,6 +722,9 @@ static float pack_islands_scale_margin(const Span islands, * - Combine results. */ + /* Workaround bug in #pack_islands_alpaca_rotate with non-square islands. */ + bool contains_non_square_islands = false; + /* First, copy information from our input into the AABB structure. */ Array aabbs(islands.size()); for (const int64_t i : islands.index_range()) { @@ -709,6 +734,9 @@ static float pack_islands_scale_margin(const Span islands, 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; + if (pack_island->aspect_y != 1.0f) { + contains_non_square_islands = true; + } } /* Sort from "biggest" to "smallest". */ @@ -785,7 +813,7 @@ static float pack_islands_scale_margin(const Span islands, /* At this stage, `max_u` and `max_v` contain the box_pack UVs. */ /* Call Alpaca. */ - if (params.rotate) { + if (params.rotate && !contains_non_square_islands) { pack_islands_alpaca_rotate(aabbs.as_mutable_span().drop_front(max_box_pack), &max_u, &max_v); } else { @@ -942,6 +970,10 @@ static float calc_margin_from_aabb_length_sum(const Span &island_v return params.margin * aabb_length_sum * 0.1f; } +/** + * Smooth differences between old API and new API by converting between storage representations. + */ + static BoxPack *pack_islands_box_array(const Span &islands, const UVPackIsland_Params ¶ms, float r_scale[2]) -- 2.30.2