UV: Add alpaca_rotate variant for improved packing efficiency
Improvements to packing efficiency and updating UV packing API. * Add #pack_islands_alpaca_rotate, the fast alpaca-style packer with rotation. * Update heuristic for sorting islands based on longest edge. (Improves pack efficiency when rotation is enabled.) * Add `aspect_y` to UV Packing API, to support non-square materials in the future. Pull Request: https://projects.blender.org/blender/blender/pulls/105977
This commit is contained in:
committed by
Chris Blackbourn
parent
00bb30c0e9
commit
b828641a93
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ class UVAABBIsland {
|
||||
float2 uv_diagonal;
|
||||
float2 uv_placement;
|
||||
int64_t index;
|
||||
float angle;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -512,7 +513,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;
|
||||
@@ -555,6 +556,154 @@ static void pack_island_xatlas(const Span<UVAABBIsland *> 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,
|
||||
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,
|
||||
@@ -573,10 +722,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()) {
|
||||
@@ -586,13 +738,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. */
|
||||
@@ -636,14 +817,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. */
|
||||
@@ -752,6 +941,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; }` */
|
||||
}
|
||||
@@ -784,16 +974,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 ¶ms,
|
||||
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;
|
||||
@@ -801,8 +995,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. */
|
||||
@@ -814,7 +1007,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. */
|
||||
@@ -823,12 +1016,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;
|
||||
@@ -843,8 +1036,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);
|
||||
|
||||
@@ -4163,6 +4163,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];
|
||||
@@ -4186,16 +4188,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;
|
||||
|
||||
Reference in New Issue
Block a user