UV: Add alpaca_rotate variant for improved packing efficiency #105977

Merged
Chris Blackbourn merged 2 commits from Chris_Blackbourn/blender:uv-alpaca-rotate3 into main 2023-03-28 00:35:38 +02:00
4 changed files with 245 additions and 32 deletions

View File

@ -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. */

View File

@ -72,9 +72,14 @@ class UVPackIsland_Params {
class PackIsland {
public:
/** Bounding rectangle of input. Will be calculated automatically in a future update. */
rctf bounds_rect;
/** Output. */
/** Aspect ratio, required for rotation. */
float aspect_y;
/** Output pre-translation. */
float2 pre_translate;
/** Output angle in radians. */
float angle;
/** Unchanged by #pack_islands, used by caller. */
int caller_index;

View File

@ -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<UVAABBIsland *> 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,154 @@ static void pack_island_xatlas(const Span<UVAABBIsland *> island_indices,
*r_max_v = max_v;
}
/**
Chris_Blackbourn marked this conversation as resolved Outdated

update_hole_rotate doesn't have a doc-string, a short explanation explaining what it's doing would be good. It's not totally obvious that q and quad_area will never be negative, that could be asserted, it could also be noted that the arguments are ordered. (to ensure unexpected negative values never occur).

`update_hole_rotate` doesn't have a doc-string, a short explanation explaining what it's doing would be good. It's not totally obvious that `q` and `quad_area` will never be negative, that could be asserted, it *could* also be noted that the arguments are ordered. (to ensure unexpected negative values never occur).
* 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,
const float u0,
const float v0,
const float u1,
const float v1)
{
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 (quad_area <= hole_area) {
return; /* No update, existing hole is larger than candidate. */
}
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 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.
*
* 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<UVAABBIsland *> 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 in the hole, no need to check for restart, or process movement. */
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;
/* Move according to the "Alpaca rules", with rotation. */
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<PackIsland *> islands,
BoxPack *box_array,
const float scale,
@ -569,10 +718,13 @@ static float pack_islands_scale_margin(const Span<PackIsland *> 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.
*/
/* 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<UVAABBIsland *> aabbs(islands.size());
for (const int64_t i : islands.index_range()) {
@ -582,13 +734,42 @@ static float pack_islands_scale_margin(const Span<PackIsland *> 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". */
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 +813,22 @@ static float pack_islands_scale_margin(const Span<PackIsland *> 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 && !contains_non_square_islands) {
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 +937,7 @@ static float pack_islands_margin_fraction(const Span<PackIsland *> &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 +970,20 @@ static float calc_margin_from_aabb_length_sum(const Span<PackIsland *> &island_v
return params.margin * aabb_length_sum * 0.1f;
}
static BoxPack *pack_islands_box_array(const Span<PackIsland *> &island_vector,
/**
* Smooth differences between old API and new API by converting between storage representations.
*/
static BoxPack *pack_islands_box_array(const Span<PackIsland *> &islands,
const UVPackIsland_Params &params,
float r_scale[2])
{
BoxPack *box_array = static_cast<BoxPack *>(
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 +991,7 @@ static BoxPack *pack_islands_box_array(const Span<PackIsland *> &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 +1003,7 @@ static BoxPack *pack_islands_box_array(const Span<PackIsland *> &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 +1012,12 @@ static BoxPack *pack_islands_box_array(const Span<PackIsland *> &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 +1032,15 @@ void pack_islands(const Span<PackIsland *> &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);

View File

@ -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;