AABB aligned quads would defeat the "witness" accelerator when using the xatlas packing strategy. This change reorders the search to allow use of the "witness" technique.
852 lines
29 KiB
C++
852 lines
29 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup eduv
|
|
*/
|
|
|
|
#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_polyfill_2d.h"
|
|
#include "BLI_polyfill_2d_beautify.h"
|
|
#include "BLI_rect.h"
|
|
#include "BLI_vector.hh"
|
|
|
|
#include "DNA_meshdata_types.h"
|
|
#include "DNA_scene_types.h"
|
|
#include "DNA_space_types.h"
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
namespace blender::geometry {
|
|
|
|
void mul_v2_m2_add_v2v2(float r[2], const float mat[2][2], const float a[2], const float b[2])
|
|
{
|
|
/* Compute `r = mat * (a + b)` with high precision.
|
|
*
|
|
* Often, linear transforms are written as:
|
|
* `A.x + b`
|
|
*
|
|
* When transforming UVs, the familiar expression can damage UVs due to round-off error,
|
|
* especially when using UDIM and if there are large numbers of islands.
|
|
*
|
|
* Instead, we provide a helper which evaluates:
|
|
* `A. (x + b)`
|
|
*
|
|
* To further reduce damage, all internal calculations are
|
|
* performed using double precision. */
|
|
|
|
const double x = double(a[0]) + double(b[0]);
|
|
const double y = double(a[1]) + double(b[1]);
|
|
|
|
r[0] = float(mat[0][0] * x + mat[1][0] * y);
|
|
r[1] = float(mat[0][1] * x + mat[1][1] * y);
|
|
}
|
|
|
|
/**
|
|
* Compute signed distance squared to a line passing through `uva` and `uvb`.
|
|
*/
|
|
static float dist_signed_squared_to_edge(float2 probe, float2 uva, float2 uvb)
|
|
{
|
|
const float2 edge = uvb - uva;
|
|
const float2 side = probe - uva;
|
|
|
|
const float edge_length_squared = blender::math::length_squared(edge);
|
|
/* Tolerance here is to avoid division by zero later. */
|
|
if (edge_length_squared < 1e-40f) {
|
|
return blender::math::length_squared(side);
|
|
}
|
|
|
|
const float numerator = edge.x * side.y - edge.y * side.x; /* c.f. cross product. */
|
|
const float numerator_ssq = numerator >= 0.0f ? numerator * numerator : -numerator * numerator;
|
|
|
|
return numerator_ssq / edge_length_squared;
|
|
}
|
|
|
|
void PackIsland::add_triangle(const float2 uv0, const float2 uv1, const float2 uv2)
|
|
{
|
|
/* Be careful with winding. */
|
|
if (dist_signed_squared_to_edge(uv0, uv1, uv2) < 0.0f) {
|
|
triangle_vertices_.append(uv0);
|
|
triangle_vertices_.append(uv1);
|
|
triangle_vertices_.append(uv2);
|
|
}
|
|
else {
|
|
triangle_vertices_.append(uv0);
|
|
triangle_vertices_.append(uv2);
|
|
triangle_vertices_.append(uv1);
|
|
}
|
|
}
|
|
|
|
void PackIsland::add_polygon(const blender::Span<float2> uvs, MemArena *arena, Heap *heap)
|
|
{
|
|
int vert_count = int(uvs.size());
|
|
BLI_assert(vert_count >= 3);
|
|
int nfilltri = vert_count - 2;
|
|
if (nfilltri == 1) {
|
|
/* Trivial case, just one triangle. */
|
|
add_triangle(uvs[0], uvs[1], uvs[2]);
|
|
return;
|
|
}
|
|
|
|
/* Storage. */
|
|
uint(*tris)[3] = static_cast<uint(*)[3]>(
|
|
BLI_memarena_alloc(arena, sizeof(*tris) * size_t(nfilltri)));
|
|
float(*source)[2] = static_cast<float(*)[2]>(
|
|
BLI_memarena_alloc(arena, sizeof(*source) * size_t(vert_count)));
|
|
|
|
/* Copy input. */
|
|
for (int i = 0; i < vert_count; i++) {
|
|
copy_v2_v2(source[i], uvs[i]);
|
|
}
|
|
|
|
/* Triangulate. */
|
|
BLI_polyfill_calc_arena(source, vert_count, 0, tris, arena);
|
|
|
|
/* Beautify improves performance of packer. (Optional)
|
|
* Long thin triangles, especially at 45 degree angles,
|
|
* can trigger worst-case performance in #trace_triangle.
|
|
* Using `Beautify` brings more inputs into average-case.
|
|
*/
|
|
BLI_polyfill_beautify(source, vert_count, tris, arena, heap);
|
|
|
|
/* Add as triangles. */
|
|
for (int j = 0; j < nfilltri; j++) {
|
|
uint *tri = tris[j];
|
|
add_triangle(source[tri[0]], source[tri[1]], source[tri[2]]);
|
|
}
|
|
|
|
BLI_heap_clear(heap, nullptr);
|
|
}
|
|
|
|
void PackIsland::finalize_geometry(const UVPackIsland_Params ¶ms, MemArena *arena, Heap *heap)
|
|
{
|
|
BLI_assert(triangle_vertices_.size() >= 3);
|
|
const eUVPackIsland_ShapeMethod shape_method = params.shape_method;
|
|
if (shape_method == ED_UVPACK_SHAPE_CONVEX) {
|
|
/* Compute convex hull of existing triangles. */
|
|
if (triangle_vertices_.size() <= 3) {
|
|
return; /* Trivial case, nothing to do. */
|
|
}
|
|
|
|
int vert_count = int(triangle_vertices_.size());
|
|
|
|
/* Allocate storage. */
|
|
int *index_map = static_cast<int *>(
|
|
BLI_memarena_alloc(arena, sizeof(*index_map) * vert_count));
|
|
|
|
/* Prepare input for convex hull. */
|
|
float(*source)[2] = reinterpret_cast<float(*)[2]>(triangle_vertices_.data());
|
|
|
|
/* Compute convex hull. */
|
|
int convex_len = BLI_convexhull_2d(source, vert_count, index_map);
|
|
|
|
/* Write back. */
|
|
triangle_vertices_.clear();
|
|
blender::Array<float2> convexVertices(convex_len);
|
|
for (int i = 0; i < convex_len; i++) {
|
|
convexVertices[i] = source[index_map[i]];
|
|
}
|
|
add_polygon(convexVertices, arena, heap);
|
|
|
|
BLI_heap_clear(heap, nullptr);
|
|
}
|
|
}
|
|
|
|
UVPackIsland_Params::UVPackIsland_Params()
|
|
{
|
|
/* TEMPORARY, set every thing to "zero" for backwards compatibility. */
|
|
rotate = false;
|
|
only_selected_uvs = false;
|
|
only_selected_faces = false;
|
|
use_seams = false;
|
|
correct_aspect = false;
|
|
ignore_pinned = false;
|
|
pin_unselected = false;
|
|
margin = 0.001f;
|
|
margin_method = ED_UVPACK_MARGIN_SCALED;
|
|
udim_base_offset[0] = 0.0f;
|
|
udim_base_offset[1] = 0.0f;
|
|
shape_method = ED_UVPACK_SHAPE_AABB;
|
|
}
|
|
|
|
/* 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<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? */
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Helper class for the `xatlas` strategy.
|
|
* Accelerates geometry queries by approximating exact queries with a bitmap.
|
|
* Includes some book keeping variables to simplify the algorithm.
|
|
*/
|
|
class Occupancy {
|
|
public:
|
|
Occupancy(const float initial_scale);
|
|
|
|
void increase_scale(); /* Resize the scale of the bitmap and clear it. */
|
|
|
|
/* Write or Query a triangle on the bitmap. */
|
|
float trace_triangle(const float2 &uv0,
|
|
const float2 &uv1,
|
|
const float2 &uv2,
|
|
const float margin,
|
|
const bool write) const;
|
|
|
|
/* Write or Query an island on the bitmap. */
|
|
float trace_island(PackIsland *island,
|
|
const float scale,
|
|
const float margin,
|
|
const float2 &uv,
|
|
const bool write) const;
|
|
|
|
int bitmap_radix; /* Width and Height of `bitmap`. */
|
|
float bitmap_scale_reciprocal; /* == 1.0f / `bitmap_scale`. */
|
|
private:
|
|
mutable blender::Array<float> bitmap_;
|
|
|
|
mutable float2 witness_; /* Witness to a previously known occupied pixel. */
|
|
mutable float witness_distance_; /* Signed distance to nearest placed island. */
|
|
mutable uint triangle_hint_; /* Hint to a previously suspected overlapping triangle. */
|
|
|
|
const float terminal = 1048576.0f; /* A "very" large number, much bigger than 4 * bitmap_radix */
|
|
};
|
|
|
|
Occupancy::Occupancy(const float initial_scale)
|
|
: bitmap_radix(800), bitmap_(bitmap_radix * bitmap_radix, false)
|
|
{
|
|
increase_scale();
|
|
bitmap_scale_reciprocal = bitmap_radix / initial_scale;
|
|
}
|
|
|
|
void Occupancy::increase_scale()
|
|
{
|
|
bitmap_scale_reciprocal *= 0.5f;
|
|
for (int i = 0; i < bitmap_radix * bitmap_radix; i++) {
|
|
bitmap_[i] = terminal;
|
|
}
|
|
witness_.x = -1;
|
|
witness_.y = -1;
|
|
witness_distance_ = 0.0f;
|
|
triangle_hint_ = 0;
|
|
}
|
|
|
|
static float signed_distance_fat_triangle(const float2 probe,
|
|
const float2 uv0,
|
|
const float2 uv1,
|
|
const float2 uv2)
|
|
{
|
|
/* Be careful with ordering, uv0 <- uv1 <- uv2 <- uv0 <- uv1 etc. */
|
|
const float dist01_ssq = dist_signed_squared_to_edge(probe, uv0, uv1);
|
|
const float dist12_ssq = dist_signed_squared_to_edge(probe, uv1, uv2);
|
|
const float dist20_ssq = dist_signed_squared_to_edge(probe, uv2, uv0);
|
|
float result_ssq = max_fff(dist01_ssq, dist12_ssq, dist20_ssq);
|
|
if (result_ssq < 0.0f) {
|
|
return -sqrtf(-result_ssq);
|
|
}
|
|
BLI_assert(result_ssq >= 0.0f);
|
|
result_ssq = std::min(result_ssq, blender::math::length_squared(probe - uv0));
|
|
result_ssq = std::min(result_ssq, blender::math::length_squared(probe - uv1));
|
|
result_ssq = std::min(result_ssq, blender::math::length_squared(probe - uv2));
|
|
BLI_assert(result_ssq >= 0.0f);
|
|
return sqrtf(result_ssq);
|
|
}
|
|
|
|
float Occupancy::trace_triangle(const float2 &uv0,
|
|
const float2 &uv1,
|
|
const float2 &uv2,
|
|
const float margin,
|
|
const bool write) const
|
|
{
|
|
const float x0 = min_fff(uv0.x, uv1.x, uv2.x);
|
|
const float y0 = min_fff(uv0.y, uv1.y, uv2.y);
|
|
const float x1 = max_fff(uv0.x, uv1.x, uv2.x);
|
|
const float y1 = max_fff(uv0.y, uv1.y, uv2.y);
|
|
float spread = write ? margin * 2 : 0.0f;
|
|
int ix0 = std::max(int(floorf((x0 - spread) * bitmap_scale_reciprocal)), 0);
|
|
int iy0 = std::max(int(floorf((y0 - spread) * bitmap_scale_reciprocal)), 0);
|
|
int ix1 = std::min(int(floorf((x1 + spread) * bitmap_scale_reciprocal + 2)), bitmap_radix);
|
|
int iy1 = std::min(int(floorf((y1 + spread) * bitmap_scale_reciprocal + 2)), bitmap_radix);
|
|
|
|
const float2 uv0s = uv0 * bitmap_scale_reciprocal;
|
|
const float2 uv1s = uv1 * bitmap_scale_reciprocal;
|
|
const float2 uv2s = uv2 * bitmap_scale_reciprocal;
|
|
|
|
/* TODO: Better epsilon handling here could reduce search size. */
|
|
float epsilon = 0.7071f; /* == sqrt(0.5f), rounded up by 0.00002f. */
|
|
epsilon = std::max(epsilon, 2 * margin * bitmap_scale_reciprocal);
|
|
|
|
if (!write) {
|
|
if (ix0 <= witness_.x && witness_.x < ix1) {
|
|
if (iy0 <= witness_.y && witness_.y < iy1) {
|
|
const float distance = signed_distance_fat_triangle(witness_, uv0s, uv1s, uv2s);
|
|
const float extent = epsilon - distance - witness_distance_;
|
|
if (extent > 0.0f) {
|
|
return extent; /* Witness observes occupied. */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Iterate in opposite direction to outer search to improve witness effectiveness. */
|
|
for (int y = iy1 - 1; y >= iy0; y--) {
|
|
for (int x = ix1 - 1; x >= ix0; x--) {
|
|
float *hotspot = &bitmap_[y * bitmap_radix + x];
|
|
if (!write && *hotspot > epsilon) {
|
|
continue;
|
|
}
|
|
const float2 probe(x, y);
|
|
const float distance = signed_distance_fat_triangle(probe, uv0s, uv1s, uv2s);
|
|
if (write) {
|
|
*hotspot = min_ff(distance, *hotspot);
|
|
continue;
|
|
}
|
|
const float extent = epsilon - distance - *hotspot;
|
|
if (extent > 0.0f) {
|
|
witness_ = probe;
|
|
witness_distance_ = *hotspot;
|
|
return extent; /* Occupied. */
|
|
}
|
|
}
|
|
}
|
|
return -1.0f; /* Available. */
|
|
}
|
|
|
|
float Occupancy::trace_island(PackIsland *island,
|
|
const float scale,
|
|
const float margin,
|
|
const float2 &uv,
|
|
const bool write) const
|
|
{
|
|
|
|
if (!write) {
|
|
if (uv.x <= 0.0f || uv.y <= 0.0f) {
|
|
return std::max(-uv.x, -uv.y); /* Occupied. */
|
|
}
|
|
}
|
|
const float2 origin(island->bounds_rect.xmin, island->bounds_rect.ymin);
|
|
const float2 delta = uv - origin * scale;
|
|
uint vert_count = uint(island->triangle_vertices_.size());
|
|
for (uint i = 0; i < vert_count; i += 3) {
|
|
uint j = (i + triangle_hint_) % vert_count;
|
|
float extent = trace_triangle(delta + island->triangle_vertices_[j] * scale,
|
|
delta + island->triangle_vertices_[j + 1] * scale,
|
|
delta + island->triangle_vertices_[j + 2] * scale,
|
|
margin,
|
|
write);
|
|
|
|
if (!write && extent >= 0.0f) {
|
|
triangle_hint_ = j;
|
|
return extent; /* Occupied. */
|
|
}
|
|
}
|
|
return -1.0f; /* Available. */
|
|
}
|
|
|
|
static float2 find_best_fit_for_island(
|
|
PackIsland *island, int scan_line, Occupancy &occupancy, const float scale, const float margin)
|
|
{
|
|
const float scan_line_bscaled = scan_line / occupancy.bitmap_scale_reciprocal;
|
|
const float size_x_scaled = BLI_rctf_size_x(&island->bounds_rect) * scale;
|
|
const float size_y_scaled = BLI_rctf_size_y(&island->bounds_rect) * scale;
|
|
|
|
/* Scan using an "Alpaca"-style search, first vertically. */
|
|
|
|
int t = 0;
|
|
while (t <= scan_line) {
|
|
const float t_bscaled = t / occupancy.bitmap_scale_reciprocal;
|
|
const float2 probe(scan_line_bscaled - size_x_scaled, t_bscaled - size_y_scaled);
|
|
const float extent = occupancy.trace_island(island, scale, margin, probe, false);
|
|
if (extent < 0.0f) {
|
|
return probe;
|
|
}
|
|
t = t + std::max(1, int(extent));
|
|
}
|
|
|
|
/* Now scan horizontally. */
|
|
t = 0;
|
|
while (t <= scan_line) {
|
|
const float t_bscaled = t / occupancy.bitmap_scale_reciprocal;
|
|
const float2 probe(t_bscaled - size_x_scaled, scan_line_bscaled - size_y_scaled);
|
|
const float extent = occupancy.trace_island(island, scale, margin, probe, false);
|
|
if (extent < 0.0f) {
|
|
return probe;
|
|
}
|
|
t = t + std::max(1, int(extent));
|
|
}
|
|
return float2(-1, -1);
|
|
}
|
|
|
|
static float guess_initial_scale(const Span<PackIsland *> islands,
|
|
const float scale,
|
|
const float margin)
|
|
{
|
|
float sum = 1e-40f;
|
|
for (int64_t i : islands.index_range()) {
|
|
PackIsland *island = islands[i];
|
|
sum += BLI_rctf_size_x(&island->bounds_rect) * scale + 2 * margin;
|
|
sum += BLI_rctf_size_y(&island->bounds_rect) * scale + 2 * margin;
|
|
}
|
|
return sqrtf(sum) / 3.0f;
|
|
}
|
|
|
|
/**
|
|
* Pack irregular islands using the `xatlas` strategy, with no rotation.
|
|
*
|
|
* Loosely based on the 'xatlas' code by Jonathan Young
|
|
* from https://github.com/jpcy/xatlas
|
|
*
|
|
* A brute force packer (BF-Packer) with accelerators:
|
|
* - Uses a Bitmap Occupancy class.
|
|
* - Uses a "Witness Pixel" and a "Triangle Hint".
|
|
* - Write with `margin * 2`, read with `margin == 0`.
|
|
* - Lazy resetting of BF search.
|
|
*
|
|
* Performance would normally be `O(n^4)`, however the occupancy
|
|
* bitmap_radix is fixed, which gives a reduced time complexity of `O(n^3)`.
|
|
*/
|
|
static void pack_island_xatlas(const Span<UVAABBIsland *> island_indices,
|
|
const Span<PackIsland *> islands,
|
|
BoxPack *box_array,
|
|
const float scale,
|
|
const float margin,
|
|
float *r_max_u,
|
|
float *r_max_v)
|
|
{
|
|
Occupancy occupancy(guess_initial_scale(islands, scale, margin));
|
|
float max_u = 0.0f;
|
|
float max_v = 0.0f;
|
|
|
|
int scan_line = 0;
|
|
int i = 0;
|
|
|
|
/* The following `while` loop is setting up a three-way race:
|
|
* `for (scan_line = 0; scan_line < bitmap_radix; scan_line++)`
|
|
* `for (i : island_indices.index_range())`
|
|
* `while (bitmap_scale_reciprocal > 0) { bitmap_scale_reciprocal *= 0.5f; }`
|
|
*/
|
|
|
|
while (i < island_indices.size()) {
|
|
PackIsland *island = islands[island_indices[i]->index];
|
|
const float2 best = find_best_fit_for_island(island, scan_line, occupancy, scale, margin);
|
|
|
|
if (best.x <= -1.0f) {
|
|
/* Unable to find a fit on this scan_line. */
|
|
|
|
if (i < 10) {
|
|
scan_line++;
|
|
}
|
|
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`.
|
|
* Possible values here *could* be 1, 2 or 3, however the only *reasonable*
|
|
* choice is 2. */
|
|
scan_line += 2;
|
|
}
|
|
if (scan_line < occupancy.bitmap_radix) {
|
|
continue; /* Try again on next scan_line. */
|
|
}
|
|
|
|
/* Enlarge search parameters. */
|
|
scan_line = 0;
|
|
occupancy.increase_scale();
|
|
|
|
/* Redraw already placed islands. (Greedy.) */
|
|
for (int j = 0; j < i; j++) {
|
|
BoxPack *box = box_array + j;
|
|
occupancy.trace_island(
|
|
islands[island_indices[j]->index], scale, margin, float2(box->x, box->y), true);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* Place island. */
|
|
BoxPack *box = box_array + i;
|
|
box->x = best.x;
|
|
box->y = best.y;
|
|
max_u = std::max(box->x + BLI_rctf_size_x(&island->bounds_rect) * scale + 2 * margin, max_u);
|
|
max_v = std::max(box->y + BLI_rctf_size_y(&island->bounds_rect) * scale + 2 * margin, max_v);
|
|
occupancy.trace_island(island, scale, margin, float2(box->x, box->y), true);
|
|
i++; /* Next island. */
|
|
|
|
if (i < 128 || (i & 31) == 16) {
|
|
scan_line = 0; /* Restart completely. */
|
|
}
|
|
else {
|
|
scan_line = std::max(0, scan_line - 25); /* `-25` must by odd. */
|
|
}
|
|
}
|
|
|
|
*r_max_u = max_u;
|
|
*r_max_v = max_v;
|
|
}
|
|
|
|
static float pack_islands_scale_margin(const Span<PackIsland *> islands,
|
|
BoxPack *box_array,
|
|
const float scale,
|
|
const float margin,
|
|
const UVPackIsland_Params ¶ms)
|
|
{
|
|
/* #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<UVAABBIsland *> 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;
|
|
}
|
|
|
|
/* 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 `islands`, largest will go to a slow packer, the rest alpaca_turbo.
|
|
* See discussion above for details. */
|
|
int64_t alpaca_cutoff = 1024; /* Regular situation, pack 1024 islands with slow packer. */
|
|
int64_t alpaca_cutoff_fast = 80; /* Reduced problem size, only 80 islands with slow packer. */
|
|
if (params.margin_method == ED_UVPACK_MARGIN_FRACTION) {
|
|
if (margin > 0.0f) {
|
|
alpaca_cutoff = alpaca_cutoff_fast;
|
|
}
|
|
}
|
|
const 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;
|
|
switch (params.shape_method) {
|
|
case ED_UVPACK_SHAPE_CONVEX:
|
|
case ED_UVPACK_SHAPE_CONCAVE:
|
|
pack_island_xatlas(aabbs.as_span().take_front(max_box_pack),
|
|
islands,
|
|
box_array,
|
|
scale,
|
|
margin,
|
|
&max_u,
|
|
&max_v);
|
|
break;
|
|
default:
|
|
BLI_box_pack_2d(box_array, int(max_box_pack), &max_u, &max_v);
|
|
break;
|
|
}
|
|
|
|
/* 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<PackIsland *> &island_vector,
|
|
BoxPack *box_array,
|
|
const float margin_fraction,
|
|
const UVPackIsland_Params ¶ms)
|
|
{
|
|
/*
|
|
* Root finding using a combined search / modified-secant method.
|
|
* First, use a robust search procedure to bracket the root within a factor of 10.
|
|
* Then, use a modified-secant method to converge.
|
|
*
|
|
* This is a specialized solver using domain knowledge to accelerate convergence. */
|
|
|
|
float scale_low = 0.0f;
|
|
float value_low = 0.0f;
|
|
float scale_high = 0.0f;
|
|
float value_high = 0.0f;
|
|
float scale_last = 0.0f;
|
|
|
|
/* Scaling smaller than `min_scale_roundoff` is unlikely to fit and
|
|
* will destroy information in existing UVs. */
|
|
const float min_scale_roundoff = 1e-5f;
|
|
|
|
/* Certain inputs might have poor convergence properties.
|
|
* Use `max_iteration` to prevent an infinite loop. */
|
|
const int max_iteration = 25;
|
|
for (int iteration = 0; iteration < max_iteration; iteration++) {
|
|
float scale = 1.0f;
|
|
|
|
if (iteration == 0) {
|
|
BLI_assert(iteration == 0);
|
|
BLI_assert(scale == 1.0f);
|
|
BLI_assert(scale_low == 0.0f);
|
|
BLI_assert(scale_high == 0.0f);
|
|
}
|
|
else if (scale_low == 0.0f) {
|
|
BLI_assert(scale_high > 0.0f);
|
|
/* Search mode, shrink layout until we can find a scale that fits. */
|
|
scale = scale_high * 0.1f;
|
|
}
|
|
else if (scale_high == 0.0f) {
|
|
BLI_assert(scale_low > 0.0f);
|
|
/* Search mode, grow layout until we can find a scale that doesn't fit. */
|
|
scale = scale_low * 10.0f;
|
|
}
|
|
else {
|
|
/* Bracket mode, use modified secant method to find root. */
|
|
BLI_assert(scale_low > 0.0f);
|
|
BLI_assert(scale_high > 0.0f);
|
|
BLI_assert(value_low <= 0.0f);
|
|
BLI_assert(value_high >= 0.0f);
|
|
if (scale_high < scale_low * 1.0001f) {
|
|
/* Convergence. */
|
|
break;
|
|
}
|
|
|
|
/* Secant method for area. */
|
|
scale = (sqrtf(scale_low) * value_high - sqrtf(scale_high) * value_low) /
|
|
(value_high - value_low);
|
|
scale = scale * scale;
|
|
|
|
if (iteration & 1) {
|
|
/* Modified binary-search to improve robustness. */
|
|
scale = sqrtf(scale * sqrtf(scale_low * scale_high));
|
|
}
|
|
}
|
|
|
|
scale = std::max(scale, min_scale_roundoff);
|
|
|
|
/* Evaluate our `f`. */
|
|
scale_last = scale;
|
|
const float max_uv = pack_islands_scale_margin(
|
|
island_vector, box_array, scale_last, margin_fraction, params);
|
|
const float value = sqrtf(max_uv) - 1.0f;
|
|
|
|
if (value <= 0.0f) {
|
|
scale_low = scale;
|
|
value_low = value;
|
|
}
|
|
else {
|
|
scale_high = scale;
|
|
value_high = value;
|
|
if (scale == min_scale_roundoff) {
|
|
/* Unable to pack without damaging UVs. */
|
|
scale_low = scale;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool flush = true;
|
|
if (flush) {
|
|
/* Write back best pack as a side-effect. First get best pack. */
|
|
if (scale_last != scale_low) {
|
|
scale_last = scale_low;
|
|
const float max_uv = pack_islands_scale_margin(
|
|
island_vector, box_array, scale_last, margin_fraction, params);
|
|
UNUSED_VARS(max_uv);
|
|
/* TODO (?): `if (max_uv < 1.0f) { scale_last /= max_uv; }` */
|
|
}
|
|
|
|
/* Then expand FaceIslands by the correct amount. */
|
|
for (const int64_t index : island_vector.index_range()) {
|
|
BoxPack *box = &box_array[index];
|
|
box->x /= scale_last;
|
|
box->y /= scale_last;
|
|
PackIsland *island = island_vector[index];
|
|
BLI_rctf_pad(
|
|
&island->bounds_rect, margin_fraction / scale_last, margin_fraction / scale_last);
|
|
}
|
|
}
|
|
return scale_last;
|
|
}
|
|
|
|
static float calc_margin_from_aabb_length_sum(const Span<PackIsland *> &island_vector,
|
|
const UVPackIsland_Params ¶ms)
|
|
{
|
|
/* Logic matches behavior from #geometry::uv_parametrizer_pack.
|
|
* Attempt to give predictable results not dependent on current UV scale by using
|
|
* `aabb_length_sum` (was "`area`") to multiply the margin by the length (was "area"). */
|
|
double aabb_length_sum = 0.0f;
|
|
for (PackIsland *island : island_vector) {
|
|
float w = BLI_rctf_size_x(&island->bounds_rect);
|
|
float h = BLI_rctf_size_y(&island->bounds_rect);
|
|
aabb_length_sum += sqrtf(w * h);
|
|
}
|
|
return params.margin * aabb_length_sum * 0.1f;
|
|
}
|
|
|
|
static BoxPack *pack_islands_box_array(const Span<PackIsland *> &island_vector,
|
|
const UVPackIsland_Params ¶ms,
|
|
float r_scale[2])
|
|
{
|
|
BoxPack *box_array = static_cast<BoxPack *>(
|
|
MEM_mallocN(sizeof(*box_array) * island_vector.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);
|
|
r_scale[0] = 1.0f / max_uv;
|
|
r_scale[1] = r_scale[0];
|
|
return box_array;
|
|
}
|
|
|
|
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);
|
|
r_scale[0] = scale;
|
|
r_scale[1] = scale;
|
|
/* pack_islands_margin_fraction will pad FaceIslands, return early. */
|
|
return box_array;
|
|
}
|
|
|
|
float margin = params.margin;
|
|
switch (params.margin_method) {
|
|
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);
|
|
break;
|
|
case ED_UVPACK_MARGIN_FRACTION: /* Added as an option in Blender 3.4. */
|
|
BLI_assert_unreachable(); /* Handled above. */
|
|
break;
|
|
default:
|
|
BLI_assert_unreachable();
|
|
}
|
|
|
|
const float max_uv = pack_islands_scale_margin(island_vector, 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];
|
|
BLI_rctf_pad(&island->bounds_rect, margin, margin);
|
|
}
|
|
return box_array;
|
|
}
|
|
|
|
void pack_islands(const Span<PackIsland *> &islands,
|
|
const UVPackIsland_Params ¶ms,
|
|
float r_scale[2])
|
|
{
|
|
BoxPack *box_array = pack_islands_box_array(islands, params, r_scale);
|
|
|
|
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;
|
|
}
|
|
|
|
MEM_freeN(box_array);
|
|
}
|
|
|
|
} // namespace blender::geometry
|