Thekla: Revert to state before clang-format changes
The file was wrongly formatted in 8b01b2e85c,
for thirdparty code we keep the upstream style *if* we need to make changes at all.
This commit is contained in:
parent
ff04c07912
commit
eb39b7f5de
3 changed files with 1218 additions and 1130 deletions
256
thirdparty/thekla_atlas/nvmesh/param/AtlasPacker.cpp
vendored
256
thirdparty/thekla_atlas/nvmesh/param/AtlasPacker.cpp
vendored
|
|
@ -3,19 +3,19 @@
|
|||
#include "nvmesh.h" // pch
|
||||
|
||||
#include "AtlasPacker.h"
|
||||
#include "nvmesh/halfedge/Face.h"
|
||||
#include "nvmesh/halfedge/Vertex.h"
|
||||
#include "nvmesh/halfedge/Face.h"
|
||||
#include "nvmesh/param/Atlas.h"
|
||||
#include "nvmesh/param/Util.h"
|
||||
#include "nvmesh/raster/Raster.h"
|
||||
|
||||
#include "nvmath/Color.h"
|
||||
#include "nvmath/ConvexHull.h"
|
||||
#include "nvmath/Vector.inl"
|
||||
#include "nvmath/ConvexHull.h"
|
||||
#include "nvmath/Color.h"
|
||||
#include "nvmath/ftoi.h"
|
||||
|
||||
#include "nvcore/StdStream.h" // fileOpen
|
||||
#include "nvcore/StrLib.h" // debug
|
||||
#include "nvcore/StdStream.h" // fileOpen
|
||||
|
||||
#include <float.h> // FLT_MAX
|
||||
#include <limits.h> // UINT_MAX
|
||||
|
|
@ -28,7 +28,8 @@ using namespace nv;
|
|||
|
||||
#include "nvimage/ImageIO.h"
|
||||
|
||||
namespace {
|
||||
namespace
|
||||
{
|
||||
const uint TGA_TYPE_GREY = 3;
|
||||
const uint TGA_TYPE_RGB = 2;
|
||||
const uint TGA_ORIGIN_UPPER = 0x20;
|
||||
|
|
@ -52,7 +53,8 @@ struct TgaHeader {
|
|||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static void outputDebugBitmap(const char *fileName, const BitMap &bitmap, int w, int h) {
|
||||
static void outputDebugBitmap(const char * fileName, const BitMap & bitmap, int w, int h)
|
||||
{
|
||||
FILE * fp = fileOpen(fileName, "wb");
|
||||
if (fp == NULL) return;
|
||||
|
||||
|
|
@ -85,7 +87,8 @@ static void outputDebugBitmap(const char *fileName, const BitMap &bitmap, int w,
|
|||
fclose(fp);
|
||||
}
|
||||
|
||||
static void outputDebugImage(const char *fileName, const Image &bitmap, int w, int h) {
|
||||
static void outputDebugImage(const char * fileName, const Image & bitmap, int w, int h)
|
||||
{
|
||||
FILE * fp = fileOpen(fileName, "wb");
|
||||
if (fp == NULL) return;
|
||||
|
||||
|
|
@ -119,7 +122,7 @@ static void outputDebugImage(const char *fileName, const Image &bitmap, int w, i
|
|||
|
||||
fclose(fp);
|
||||
}
|
||||
} // namespace
|
||||
}
|
||||
|
||||
#endif // DEBUG_OUTPUT
|
||||
|
||||
|
|
@ -133,22 +136,24 @@ inline bool isAligned(int x, int a) {
|
|||
return (x & (a - 1)) == 0;
|
||||
}
|
||||
|
||||
AtlasPacker::AtlasPacker(Atlas *atlas) :
|
||||
m_atlas(atlas),
|
||||
m_bitmap(256, 256) {
|
||||
|
||||
|
||||
AtlasPacker::AtlasPacker(Atlas * atlas) : m_atlas(atlas), m_bitmap(256, 256)
|
||||
{
|
||||
m_width = 0;
|
||||
m_height = 0;
|
||||
#if 0
|
||||
|
||||
m_debug_bitmap.allocate(256, 256);
|
||||
m_debug_bitmap.fill(Color32(0,0,0,0));
|
||||
#endif
|
||||
}
|
||||
|
||||
AtlasPacker::~AtlasPacker() {
|
||||
AtlasPacker::~AtlasPacker()
|
||||
{
|
||||
}
|
||||
|
||||
// This should compute convex hull and use rotating calipers to find the best box. Currently it uses a brute force method.
|
||||
static bool computeBoundingBox(Chart *chart, Vector2 *majorAxis, Vector2 *minorAxis, Vector2 *minCorner, Vector2 *maxCorner) {
|
||||
static bool computeBoundingBox(Chart * chart, Vector2 * majorAxis, Vector2 * minorAxis, Vector2 * minCorner, Vector2 * maxCorner)
|
||||
{
|
||||
// Compute list of boundary points.
|
||||
Array<Vector2> points(16);
|
||||
|
||||
|
|
@ -276,23 +281,28 @@ static bool computeBoundingBox(Chart *chart, Vector2 *majorAxis, Vector2 *minorA
|
|||
Vector2 maxs[N];
|
||||
|
||||
const int iterationCount = 1;
|
||||
for (int j = 0; j < iterationCount; j++) {
|
||||
for (int j = 0; j < iterationCount; j++)
|
||||
{
|
||||
// Init predefined directions.
|
||||
for (int i = 0; i < N; i++) {
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
float angle = lerp(minAngle, maxAngle, float(i)/N);
|
||||
axis[i].set(cosf(angle), sinf(angle));
|
||||
}
|
||||
|
||||
// Compute box for each direction.
|
||||
for (int i = 0; i < N; i++) {
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
mins[i].set(FLT_MAX, FLT_MAX);
|
||||
maxs[i].set(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
|
||||
for (uint p = 0; p < points.count(); p++) {
|
||||
for (uint p = 0; p < points.count(); p++)
|
||||
{
|
||||
Vector2 point = points[p];
|
||||
|
||||
for (int i = 0; i < N; i++) {
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
float x = dot(axis[i], point);
|
||||
if (x < mins[i].x) mins[i].x = x;
|
||||
if (x > maxs[i].x) maxs[i].x = x;
|
||||
|
|
@ -309,16 +319,20 @@ static bool computeBoundingBox(Chart *chart, Vector2 *majorAxis, Vector2 *minorA
|
|||
float best_area = FLT_MAX;
|
||||
float second_best_area = FLT_MAX;
|
||||
|
||||
for (int i = 0; i < N; i++) {
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
float area = (maxs[i].x - mins[i].x) * (maxs[i].y - mins[i].y);
|
||||
|
||||
if (area < best_area) {
|
||||
if (area < best_area)
|
||||
{
|
||||
second_best_area = best_area;
|
||||
second_best = best;
|
||||
|
||||
best_area = area;
|
||||
best = i;
|
||||
} else if (area < second_best_area) {
|
||||
}
|
||||
else if (area < second_best_area)
|
||||
{
|
||||
second_best_area = area;
|
||||
second_best = i;
|
||||
}
|
||||
|
|
@ -327,7 +341,8 @@ static bool computeBoundingBox(Chart *chart, Vector2 *majorAxis, Vector2 *minorA
|
|||
nvDebugCheck(second_best != -1);
|
||||
nvDebugCheck(best != second_best);
|
||||
|
||||
if (j != iterationCount - 1) {
|
||||
if (j != iterationCount-1)
|
||||
{
|
||||
// Handle wrap-around during the first iteration.
|
||||
if (j == 0) {
|
||||
if (best == 0 && second_best == N-1) best = N;
|
||||
|
|
@ -365,7 +380,9 @@ static bool computeBoundingBox(Chart *chart, Vector2 *majorAxis, Vector2 *minorA
|
|||
return true;
|
||||
}
|
||||
|
||||
void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned, bool conservative) {
|
||||
|
||||
void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned, bool conservative)
|
||||
{
|
||||
const uint chartCount = m_atlas->chartCount();
|
||||
if (chartCount == 0) return;
|
||||
|
||||
|
|
@ -376,7 +393,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
chartExtents.resize(chartCount);
|
||||
|
||||
float meshArea = 0;
|
||||
for (uint c = 0; c < chartCount; c++) {
|
||||
for (uint c = 0; c < chartCount; c++)
|
||||
{
|
||||
Chart * chart = m_atlas->chartAt(c);
|
||||
|
||||
if (!chart->isVertexMapped() && !chart->isDisk()) {
|
||||
|
|
@ -395,7 +413,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
// Arrange vertices in a rectangle.
|
||||
extents.x = float(chart->vertexMapWidth);
|
||||
extents.y = float(chart->vertexMapHeight);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// Compute surface area to sort charts.
|
||||
float chartArea = chart->computeSurfaceArea();
|
||||
meshArea += chartArea;
|
||||
|
|
@ -430,7 +449,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
// Translate, rotate and scale vertices. Compute extents.
|
||||
HalfEdge::Mesh * mesh = chart->chartMesh();
|
||||
const uint vertexCount = mesh->vertexCount();
|
||||
for (uint i = 0; i < vertexCount; i++) {
|
||||
for (uint i = 0; i < vertexCount; i++)
|
||||
{
|
||||
HalfEdge::Vertex * vertex = mesh->vertexAt(i);
|
||||
|
||||
//Vector2 t = vertex->tex - origin;
|
||||
|
|
@ -463,7 +483,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
|
||||
scale = 1024 / (limit + 1);
|
||||
|
||||
for (uint i = 0; i < vertexCount; i++) {
|
||||
for (uint i = 0; i < vertexCount; i++)
|
||||
{
|
||||
HalfEdge::Vertex * vertex = mesh->vertexAt(i);
|
||||
vertex->tex *= scale;
|
||||
}
|
||||
|
|
@ -473,6 +494,7 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
nvDebugCheck(extents.x <= 1024 && extents.y <= 1024);
|
||||
}
|
||||
|
||||
|
||||
// Scale the charts to use the entire texel area available. So, if the width is 0.1 we could scale it to 1 without increasing the lightmap usage and making a better
|
||||
// use of it. In many cases this also improves the look of the seams, since vertices on the chart boundaries have more chances of being aligned with the texel centers.
|
||||
|
||||
|
|
@ -489,7 +511,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
// Align all chart extents to 4x4 blocks, but taking padding into account.
|
||||
if (conservative) {
|
||||
cw = align(cw + 2, 4) - 2;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
cw = align(cw + 1, 4) - 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -506,7 +529,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
// Align all chart extents to 4x4 blocks, but taking padding into account.
|
||||
if (conservative) {
|
||||
ch = align(ch + 2, 4) - 2;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
ch = align(ch + 1, 4) - 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -567,22 +591,24 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
}
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
// Init bit map.
|
||||
m_bitmap.clearAll();
|
||||
if (approximateExtent > m_bitmap.width()) {
|
||||
m_bitmap.resize(approximateExtent, approximateExtent, false);
|
||||
#if 0
|
||||
m_debug_bitmap.resize(approximateExtent, approximateExtent);
|
||||
m_debug_bitmap.fill(Color32(0,0,0,0));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
|
||||
#if 1
|
||||
// Add sorted charts to bitmap.
|
||||
for (uint i = 0; i < chartCount; i++) {
|
||||
for (uint i = 0; i < chartCount; i++)
|
||||
{
|
||||
uint c = ranks[chartCount - i - 1]; // largest chart first
|
||||
|
||||
Chart * chart = m_atlas->chartAt(c);
|
||||
|
|
@ -599,7 +625,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
chart_bitmap.resize(ftoi_ceil(chartExtents[c].x), ftoi_ceil(chartExtents[c].y), /*initValue=*/true);
|
||||
|
||||
// @@ Another alternative would be to try to map each vertex to a different texel trying to fill all the available unused texels.
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// @@ Add special cases for dot and line charts. @@ Lightmap rasterizer also needs to handle these special cases.
|
||||
// @@ We could also have a special case for chart quads. If the quad surface <= 4 texels, align vertices with texel centers and do not add padding. May be very useful for foliage.
|
||||
|
||||
|
|
@ -621,7 +648,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
|
||||
// Rasterize chart and dilate.
|
||||
drawChartBitmapDilate(chart, &chart_bitmap, /*padding=*/1);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
// Init all bits to 0.
|
||||
chart_bitmap.resize(ftoi_ceil(chartExtents[c].x) + 1, ftoi_ceil(chartExtents[c].y) + 1, /*initValue=*/false); // Add half a texels on each side.
|
||||
|
||||
|
|
@ -648,12 +676,11 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
h = align(h, 4);
|
||||
|
||||
// Resize bitmap if necessary.
|
||||
if (uint(w) > m_bitmap.width() || uint(h) > m_bitmap.height()) {
|
||||
if (uint(w) > m_bitmap.width() || uint(h) > m_bitmap.height())
|
||||
{
|
||||
//nvDebug("Resize bitmap (%d, %d).\n", nextPowerOfTwo(w), nextPowerOfTwo(h));
|
||||
m_bitmap.resize(nextPowerOfTwo(U32(w)), nextPowerOfTwo(U32(h)), false);
|
||||
#if 0
|
||||
m_debug_bitmap.resize(nextPowerOfTwo(U32(w)), nextPowerOfTwo(U32(h)));
|
||||
#endif
|
||||
}
|
||||
|
||||
//nvDebug("Add chart at (%d, %d).\n", best_x, best_y);
|
||||
|
|
@ -661,20 +688,20 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
addChart(&chart_bitmap, w, h, best_x, best_y, best_r, /*debugOutput=*/NULL);
|
||||
|
||||
// IC: Output chart again to debug bitmap.
|
||||
#if 0
|
||||
if (chart->isVertexMapped()) {
|
||||
addChart(&chart_bitmap, w, h, best_x, best_y, best_r, &m_debug_bitmap);
|
||||
}
|
||||
else {
|
||||
addChart(chart, w, h, best_x, best_y, best_r, &m_debug_bitmap);
|
||||
}
|
||||
#endif
|
||||
|
||||
//float best_angle = 2 * PI * best_r;
|
||||
|
||||
// Translate and rotate chart texture coordinates.
|
||||
HalfEdge::Mesh * mesh = chart->chartMesh();
|
||||
const uint vertexCount = mesh->vertexCount();
|
||||
for (uint v = 0; v < vertexCount; v++) {
|
||||
for (uint v = 0; v < vertexCount; v++)
|
||||
{
|
||||
HalfEdge::Vertex * vertex = mesh->vertexAt(v);
|
||||
|
||||
Vector2 t = vertex->tex;
|
||||
|
|
@ -700,7 +727,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
#else // 0
|
||||
|
||||
// Add sorted charts to bitmap.
|
||||
for (uint i = 0; i < chartCount; i++) {
|
||||
for (uint i = 0; i < chartCount; i++)
|
||||
{
|
||||
uint c = ranks[chartCount - i - 1]; // largest chart first
|
||||
|
||||
Chart * chart = m_atlas->chartAt(c);
|
||||
|
|
@ -709,8 +737,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
|
||||
Vector2 scale(1, 1);
|
||||
|
||||
#if 0 // old method. \
|
||||
//m_padding_x = 2*padding; \
|
||||
#if 0 // old method.
|
||||
//m_padding_x = 2*padding;
|
||||
//m_padding_y = 2*padding;
|
||||
#else
|
||||
//m_padding_x = 0; //padding;
|
||||
|
|
@ -755,7 +783,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
h = max(h, best_y + best_ch);
|
||||
|
||||
// Resize bitmap if necessary.
|
||||
if (uint(w) > m_bitmap.width() || uint(h) > m_bitmap.height()) {
|
||||
if (uint(w) > m_bitmap.width() || uint(h) > m_bitmap.height())
|
||||
{
|
||||
//nvDebug("Resize bitmap (%d, %d).\n", nextPowerOfTwo(w), nextPowerOfTwo(h));
|
||||
m_bitmap.resize(nextPowerOfTwo(w), nextPowerOfTwo(h), false);
|
||||
m_debug_bitmap.resize(nextPowerOfTwo(w), nextPowerOfTwo(h));
|
||||
|
|
@ -780,7 +809,8 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
// Translate and rotate chart texture coordinates.
|
||||
HalfEdge::Mesh * mesh = chart->chartMesh();
|
||||
const uint vertexCount = mesh->vertexCount();
|
||||
for (uint v = 0; v < vertexCount; v++) {
|
||||
for (uint v = 0; v < vertexCount; v++)
|
||||
{
|
||||
HalfEdge::Vertex * vertex = mesh->vertexAt(v);
|
||||
|
||||
Vector2 t = vertex->tex * scale + offset;
|
||||
|
|
@ -811,10 +841,10 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
|
||||
nvCheck(isAligned(m_width, 4));
|
||||
nvCheck(isAligned(m_height, 4));
|
||||
#if 0
|
||||
|
||||
m_debug_bitmap.resize(m_width, m_height);
|
||||
m_debug_bitmap.setFormat(Image::Format_ARGB);
|
||||
#endif
|
||||
|
||||
#if DEBUG_OUTPUT
|
||||
//outputDebugBitmap("debug_packer_final.tga", m_bitmap, w, h);
|
||||
//outputDebugImage("debug_packer_final.tga", m_debug_bitmap, w, h);
|
||||
|
|
@ -822,31 +852,38 @@ void AtlasPacker::packCharts(int quality, float texelsPerUnit, bool blockAligned
|
|||
#endif
|
||||
}
|
||||
|
||||
|
||||
// IC: Brute force is slow, and random may take too much time to converge. We start inserting large charts in a small atlas. Using brute force is lame, because most of the space
|
||||
// is occupied at this point. At the end we have many small charts and a large atlas with sparse holes. Finding those holes randomly is slow. A better approach would be to
|
||||
// start stacking large charts as if they were tetris pieces. Once charts get small try to place them randomly. It may be interesting to try a intermediate strategy, first try
|
||||
// along one axis and then try exhaustively along that axis.
|
||||
void AtlasPacker::findChartLocation(int quality, const BitMap *bitmap, Vector2::Arg extents, int w, int h, int *best_x, int *best_y, int *best_w, int *best_h, int *best_r) {
|
||||
void AtlasPacker::findChartLocation(int quality, const BitMap * bitmap, Vector2::Arg extents, int w, int h, int * best_x, int * best_y, int * best_w, int * best_h, int * best_r)
|
||||
{
|
||||
int attempts = 256;
|
||||
if (quality == 1) attempts = 4096;
|
||||
if (quality == 2) attempts = 2048;
|
||||
if (quality == 3) attempts = 1024;
|
||||
if (quality == 4) attempts = 512;
|
||||
|
||||
if (quality == 0 || w * h < attempts) {
|
||||
if (quality == 0 || w*h < attempts)
|
||||
{
|
||||
findChartLocation_bruteForce(bitmap, extents, w, h, best_x, best_y, best_w, best_h, best_r);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
findChartLocation_random(bitmap, extents, w, h, best_x, best_y, best_w, best_h, best_r, attempts);
|
||||
}
|
||||
}
|
||||
|
||||
#define BLOCK_SIZE 4
|
||||
|
||||
void AtlasPacker::findChartLocation_bruteForce(const BitMap *bitmap, Vector2::Arg extents, int w, int h, int *best_x, int *best_y, int *best_w, int *best_h, int *best_r) {
|
||||
void AtlasPacker::findChartLocation_bruteForce(const BitMap * bitmap, Vector2::Arg extents, int w, int h, int * best_x, int * best_y, int * best_w, int * best_h, int * best_r)
|
||||
{
|
||||
int best_metric = INT_MAX;
|
||||
|
||||
// Try two different orientations.
|
||||
for (int r = 0; r < 2; r++) {
|
||||
for (int r = 0; r < 2; r++)
|
||||
{
|
||||
int cw = bitmap->width();
|
||||
int ch = bitmap->height();
|
||||
if (r & 1) swap(cw, ch);
|
||||
|
|
@ -870,7 +907,8 @@ void AtlasPacker::findChartLocation_bruteForce(const BitMap *bitmap, Vector2::Ar
|
|||
continue;
|
||||
}
|
||||
|
||||
if (canAddChart(bitmap, w, h, x, y, r)) {
|
||||
if (canAddChart(bitmap, w, h, x, y, r))
|
||||
{
|
||||
best_metric = metric;
|
||||
*best_x = x;
|
||||
*best_y = y;
|
||||
|
|
@ -878,7 +916,8 @@ void AtlasPacker::findChartLocation_bruteForce(const BitMap *bitmap, Vector2::Ar
|
|||
*best_h = ch;
|
||||
*best_r = r;
|
||||
|
||||
if (area == w * h) {
|
||||
if (area == w*h)
|
||||
{
|
||||
// Chart is completely inside, do not look at any other location.
|
||||
goto done;
|
||||
}
|
||||
|
|
@ -891,10 +930,13 @@ done:
|
|||
nvDebugCheck (best_metric != INT_MAX);
|
||||
}
|
||||
|
||||
void AtlasPacker::findChartLocation_random(const BitMap *bitmap, Vector2::Arg extents, int w, int h, int *best_x, int *best_y, int *best_w, int *best_h, int *best_r, int minTrialCount) {
|
||||
|
||||
void AtlasPacker::findChartLocation_random(const BitMap * bitmap, Vector2::Arg extents, int w, int h, int * best_x, int * best_y, int * best_w, int * best_h, int * best_r, int minTrialCount)
|
||||
{
|
||||
int best_metric = INT_MAX;
|
||||
|
||||
for (int i = 0; i < minTrialCount || best_metric == INT_MAX; i++) {
|
||||
for (int i = 0; i < minTrialCount || best_metric == INT_MAX; i++)
|
||||
{
|
||||
int r = m_rand.getRange(1);
|
||||
int x = m_rand.getRange(w + 1); // + 1 to extend atlas in case atlas full. We may want to use a higher number to increase probability of extending atlas.
|
||||
int y = m_rand.getRange(h + 1); // + 1 to extend atlas in case atlas full.
|
||||
|
|
@ -921,7 +963,8 @@ void AtlasPacker::findChartLocation_random(const BitMap *bitmap, Vector2::Arg ex
|
|||
continue;
|
||||
}
|
||||
|
||||
if (canAddChart(bitmap, w, h, x, y, r)) {
|
||||
if (canAddChart(bitmap, w, h, x, y, r))
|
||||
{
|
||||
best_metric = metric;
|
||||
*best_x = x;
|
||||
*best_y = y;
|
||||
|
|
@ -929,7 +972,8 @@ void AtlasPacker::findChartLocation_random(const BitMap *bitmap, Vector2::Arg ex
|
|||
*best_h = ch;
|
||||
*best_r = r;
|
||||
|
||||
if (area == w * h) {
|
||||
if (area == w*h)
|
||||
{
|
||||
// Chart is completely inside, do not look at any other location.
|
||||
break;
|
||||
}
|
||||
|
|
@ -937,29 +981,37 @@ void AtlasPacker::findChartLocation_random(const BitMap *bitmap, Vector2::Arg ex
|
|||
}
|
||||
}
|
||||
|
||||
void AtlasPacker::drawChartBitmapDilate(const Chart *chart, BitMap *bitmap, int padding) {
|
||||
|
||||
void AtlasPacker::drawChartBitmapDilate(const Chart * chart, BitMap * bitmap, int padding)
|
||||
{
|
||||
const int w = bitmap->width();
|
||||
const int h = bitmap->height();
|
||||
const Vector2 extents = Vector2(float(w), float(h));
|
||||
|
||||
// Rasterize chart faces, check that all bits are not set.
|
||||
const uint faceCount = chart->faceCount();
|
||||
for (uint f = 0; f < faceCount; f++) {
|
||||
for (uint f = 0; f < faceCount; f++)
|
||||
{
|
||||
const HalfEdge::Face * face = chart->chartMesh()->faceAt(f);
|
||||
|
||||
Vector2 vertices[4];
|
||||
|
||||
uint edgeCount = 0;
|
||||
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance()) {
|
||||
if (edgeCount < 4) {
|
||||
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
||||
{
|
||||
if (edgeCount < 4)
|
||||
{
|
||||
vertices[edgeCount] = it.vertex()->tex + Vector2(0.5) + Vector2(float(padding), float(padding));
|
||||
}
|
||||
edgeCount++;
|
||||
}
|
||||
|
||||
if (edgeCount == 3) {
|
||||
if (edgeCount == 3)
|
||||
{
|
||||
Raster::drawTriangle(Raster::Mode_Antialiased, extents, true, vertices, AtlasPacker::setBitsCallback, bitmap);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
Raster::drawQuad(Raster::Mode_Antialiased, extents, true, vertices, AtlasPacker::setBitsCallback, bitmap);
|
||||
}
|
||||
}
|
||||
|
|
@ -994,7 +1046,9 @@ void AtlasPacker::drawChartBitmapDilate(const Chart *chart, BitMap *bitmap, int
|
|||
}
|
||||
}
|
||||
|
||||
void AtlasPacker::drawChartBitmap(const Chart *chart, BitMap *bitmap, const Vector2 &scale, const Vector2 &offset) {
|
||||
|
||||
void AtlasPacker::drawChartBitmap(const Chart * chart, BitMap * bitmap, const Vector2 & scale, const Vector2 & offset)
|
||||
{
|
||||
const int w = bitmap->width();
|
||||
const int h = bitmap->height();
|
||||
const Vector2 extents = Vector2(float(w), float(h));
|
||||
|
|
@ -1017,14 +1071,17 @@ void AtlasPacker::drawChartBitmap(const Chart *chart, BitMap *bitmap, const Vect
|
|||
|
||||
// Rasterize chart faces, check that all bits are not set.
|
||||
const uint faceCount = chart->chartMesh()->faceCount();
|
||||
for (uint f = 0; f < faceCount; f++) {
|
||||
for (uint f = 0; f < faceCount; f++)
|
||||
{
|
||||
const HalfEdge::Face * face = chart->chartMesh()->faceAt(f);
|
||||
|
||||
Vector2 vertices[4];
|
||||
|
||||
uint edgeCount = 0;
|
||||
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance()) {
|
||||
if (edgeCount < 4) {
|
||||
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
||||
{
|
||||
if (edgeCount < 4)
|
||||
{
|
||||
vertices[edgeCount] = it.vertex()->tex * scale + offset + pad[i];
|
||||
nvCheck(ftoi_ceil(vertices[edgeCount].x) >= 0);
|
||||
nvCheck(ftoi_ceil(vertices[edgeCount].y) >= 0);
|
||||
|
|
@ -1034,9 +1091,12 @@ void AtlasPacker::drawChartBitmap(const Chart *chart, BitMap *bitmap, const Vect
|
|||
edgeCount++;
|
||||
}
|
||||
|
||||
if (edgeCount == 3) {
|
||||
if (edgeCount == 3)
|
||||
{
|
||||
Raster::drawTriangle(Raster::Mode_Antialiased, extents, /*enableScissors=*/true, vertices, AtlasPacker::setBitsCallback, bitmap);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
Raster::drawQuad(Raster::Mode_Antialiased, extents, /*enableScissors=*/true, vertices, AtlasPacker::setBitsCallback, bitmap);
|
||||
}
|
||||
}
|
||||
|
|
@ -1076,7 +1136,8 @@ void AtlasPacker::drawChartBitmap(const Chart *chart, BitMap *bitmap, const Vect
|
|||
}
|
||||
}
|
||||
|
||||
bool AtlasPacker::canAddChart(const BitMap *bitmap, int atlas_w, int atlas_h, int offset_x, int offset_y, int r) {
|
||||
bool AtlasPacker::canAddChart(const BitMap * bitmap, int atlas_w, int atlas_h, int offset_x, int offset_y, int r)
|
||||
{
|
||||
nvDebugCheck(r == 0 || r == 1);
|
||||
|
||||
// Check whether the two bitmaps overlap.
|
||||
|
|
@ -1100,7 +1161,8 @@ bool AtlasPacker::canAddChart(const BitMap *bitmap, int atlas_w, int atlas_h, in
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (r == 1) {
|
||||
}
|
||||
else if (r == 1) {
|
||||
for (int y = 0; y < h; y++) {
|
||||
int xx = y + offset_x;
|
||||
if (xx >= 0) {
|
||||
|
|
@ -1160,6 +1222,7 @@ void AtlasPacker::checkCanAddChart(const Chart * chart, int w, int h, int x, int
|
|||
}
|
||||
#endif // 0
|
||||
|
||||
|
||||
static Color32 chartColor = Color32(0);
|
||||
static void selectRandomColor(MTRand & rand) {
|
||||
// Pick random color for this chart. @@ Select random hue, but fixed saturation/luminance?
|
||||
|
|
@ -1168,7 +1231,8 @@ static void selectRandomColor(MTRand &rand) {
|
|||
chartColor.b = 128 + rand.getRange(127);
|
||||
chartColor.a = 255;
|
||||
}
|
||||
static bool debugDrawCallback(void *param, int x, int y, Vector3::Arg, Vector3::Arg, Vector3::Arg, float area) {
|
||||
static bool debugDrawCallback(void * param, int x, int y, Vector3::Arg, Vector3::Arg, Vector3::Arg, float area)
|
||||
{
|
||||
Image * image = (Image *)param;
|
||||
|
||||
if (area > 0.0) {
|
||||
|
|
@ -1183,7 +1247,8 @@ static bool debugDrawCallback(void *param, int x, int y, Vector3::Arg, Vector3::
|
|||
return true;
|
||||
}
|
||||
|
||||
void AtlasPacker::addChart(const Chart *chart, int w, int h, int x, int y, int r, Image *debugOutput) {
|
||||
void AtlasPacker::addChart(const Chart * chart, int w, int h, int x, int y, int r, Image * debugOutput)
|
||||
{
|
||||
nvDebugCheck(r == 0 || r == 1);
|
||||
|
||||
nvDebugCheck(debugOutput != NULL);
|
||||
|
|
@ -1194,14 +1259,17 @@ void AtlasPacker::addChart(const Chart *chart, int w, int h, int x, int y, int r
|
|||
|
||||
// Rasterize chart faces, set bits.
|
||||
const uint faceCount = chart->faceCount();
|
||||
for (uint f = 0; f < faceCount; f++) {
|
||||
for (uint f = 0; f < faceCount; f++)
|
||||
{
|
||||
const HalfEdge::Face * face = chart->chartMesh()->faceAt(f);
|
||||
|
||||
Vector2 vertices[4];
|
||||
|
||||
uint edgeCount = 0;
|
||||
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance()) {
|
||||
if (edgeCount < 4) {
|
||||
for (HalfEdge::Face::ConstEdgeIterator it(face->edges()); !it.isDone(); it.advance())
|
||||
{
|
||||
if (edgeCount < 4)
|
||||
{
|
||||
Vector2 t = it.vertex()->tex;
|
||||
if (r == 1) swap(t.x, t.y);
|
||||
vertices[edgeCount] = t + offset;
|
||||
|
|
@ -1209,15 +1277,20 @@ void AtlasPacker::addChart(const Chart *chart, int w, int h, int x, int y, int r
|
|||
edgeCount++;
|
||||
}
|
||||
|
||||
if (edgeCount == 3) {
|
||||
if (edgeCount == 3)
|
||||
{
|
||||
Raster::drawTriangle(Raster::Mode_Antialiased, extents, /*enableScissors=*/true, vertices, debugDrawCallback, debugOutput);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
Raster::drawQuad(Raster::Mode_Antialiased, extents, /*enableScissors=*/true, vertices, debugDrawCallback, debugOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AtlasPacker::addChart(const BitMap *bitmap, int atlas_w, int atlas_h, int offset_x, int offset_y, int r, Image *debugOutput) {
|
||||
|
||||
void AtlasPacker::addChart(const BitMap * bitmap, int atlas_w, int atlas_h, int offset_x, int offset_y, int r, Image * debugOutput)
|
||||
{
|
||||
nvDebugCheck(r == 0 || r == 1);
|
||||
|
||||
// Check whether the two bitmaps overlap.
|
||||
|
|
@ -1238,8 +1311,7 @@ void AtlasPacker::addChart(const BitMap *bitmap, int atlas_w, int atlas_h, int o
|
|||
if (xx >= 0) {
|
||||
if (bitmap->bitAt(x, y)) {
|
||||
if (xx < atlas_w && yy < atlas_h) {
|
||||
if (debugOutput)
|
||||
debugOutput->pixel(xx, yy) = chartColor;
|
||||
if (debugOutput) debugOutput->pixel(xx, yy) = chartColor;
|
||||
else {
|
||||
nvDebugCheck(m_bitmap.bitAt(xx, yy) == false);
|
||||
m_bitmap.setBitAt(xx, yy);
|
||||
|
|
@ -1250,7 +1322,8 @@ void AtlasPacker::addChart(const BitMap *bitmap, int atlas_w, int atlas_h, int o
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (r == 1) {
|
||||
}
|
||||
else if (r == 1) {
|
||||
for (int y = 0; y < h; y++) {
|
||||
int xx = y + offset_x;
|
||||
if (xx >= 0) {
|
||||
|
|
@ -1259,8 +1332,7 @@ void AtlasPacker::addChart(const BitMap *bitmap, int atlas_w, int atlas_h, int o
|
|||
if (yy >= 0) {
|
||||
if (bitmap->bitAt(x, y)) {
|
||||
if (xx < atlas_w && yy < atlas_h) {
|
||||
if (debugOutput)
|
||||
debugOutput->pixel(xx, yy) = chartColor;
|
||||
if (debugOutput) debugOutput->pixel(xx, yy) = chartColor;
|
||||
else {
|
||||
nvDebugCheck(m_bitmap.bitAt(xx, yy) == false);
|
||||
m_bitmap.setBitAt(xx, yy);
|
||||
|
|
@ -1274,7 +1346,10 @@ void AtlasPacker::addChart(const BitMap *bitmap, int atlas_w, int atlas_h, int o
|
|||
}
|
||||
}
|
||||
|
||||
/*static*/ bool AtlasPacker::checkBitsCallback(void *param, int x, int y, Vector3::Arg, Vector3::Arg, Vector3::Arg, float) {
|
||||
|
||||
|
||||
/*static*/ bool AtlasPacker::checkBitsCallback(void * param, int x, int y, Vector3::Arg, Vector3::Arg, Vector3::Arg, float)
|
||||
{
|
||||
BitMap * bitmap = (BitMap * )param;
|
||||
|
||||
nvDebugCheck(bitmap->bitAt(x, y) == false);
|
||||
|
|
@ -1282,7 +1357,8 @@ void AtlasPacker::addChart(const BitMap *bitmap, int atlas_w, int atlas_h, int o
|
|||
return true;
|
||||
}
|
||||
|
||||
/*static*/ bool AtlasPacker::setBitsCallback(void *param, int x, int y, Vector3::Arg, Vector3::Arg, Vector3::Arg, float area) {
|
||||
/*static*/ bool AtlasPacker::setBitsCallback(void * param, int x, int y, Vector3::Arg, Vector3::Arg, Vector3::Arg, float area)
|
||||
{
|
||||
BitMap * bitmap = (BitMap * )param;
|
||||
|
||||
if (area > 0.0) {
|
||||
|
|
@ -1292,6 +1368,8 @@ void AtlasPacker::addChart(const BitMap *bitmap, int atlas_w, int atlas_h, int o
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
float AtlasPacker::computeAtlasUtilization() const {
|
||||
const uint w = m_width;
|
||||
const uint h = m_height;
|
||||
|
|
|
|||
|
|
@ -5,18 +5,21 @@
|
|||
#define NV_MESH_ATLASPACKER_H
|
||||
|
||||
#include "nvcore/RadixSort.h"
|
||||
#include "nvmath/Vector.h"
|
||||
#include "nvmath/Random.h"
|
||||
#include "nvimage/BitMap.h"
|
||||
#include "nvimage/Image.h"
|
||||
#include "nvmath/Random.h"
|
||||
#include "nvmath/Vector.h"
|
||||
|
||||
#include "nvmesh/nvmesh.h"
|
||||
|
||||
namespace nv {
|
||||
|
||||
namespace nv
|
||||
{
|
||||
class Atlas;
|
||||
class Chart;
|
||||
|
||||
struct AtlasPacker {
|
||||
struct AtlasPacker
|
||||
{
|
||||
AtlasPacker(Atlas * atlas);
|
||||
~AtlasPacker();
|
||||
|
||||
|
|
@ -24,6 +27,7 @@ struct AtlasPacker {
|
|||
float computeAtlasUtilization() const;
|
||||
|
||||
private:
|
||||
|
||||
void findChartLocation(int quality, const BitMap * bitmap, Vector2::Arg extents, int w, int h, int * best_x, int * best_y, int * best_w, int * best_h, int * best_r);
|
||||
void findChartLocation_bruteForce(const BitMap * bitmap, Vector2::Arg extents, int w, int h, int * best_x, int * best_y, int * best_w, int * best_h, int * best_r);
|
||||
void findChartLocation_random(const BitMap * bitmap, Vector2::Arg extents, int w, int h, int * best_x, int * best_y, int * best_w, int * best_h, int * best_r, int minTrialCount);
|
||||
|
|
@ -36,21 +40,24 @@ private:
|
|||
//void checkCanAddChart(const Chart * chart, int w, int h, int x, int y, int r);
|
||||
void addChart(const Chart * chart, int w, int h, int x, int y, int r, Image * debugOutput);
|
||||
|
||||
|
||||
static bool checkBitsCallback(void * param, int x, int y, Vector3::Arg bar, Vector3::Arg dx, Vector3::Arg dy, float coverage);
|
||||
static bool setBitsCallback(void * param, int x, int y, Vector3::Arg bar, Vector3::Arg dx, Vector3::Arg dy, float coverage);
|
||||
|
||||
private:
|
||||
|
||||
Atlas * m_atlas;
|
||||
BitMap m_bitmap;
|
||||
//Image m_debug_bitmap;
|
||||
Image m_debug_bitmap;
|
||||
RadixSort m_radix;
|
||||
|
||||
uint m_width;
|
||||
uint m_height;
|
||||
|
||||
MTRand m_rand;
|
||||
|
||||
};
|
||||
|
||||
} // namespace nv
|
||||
} // nv namespace
|
||||
|
||||
#endif // NV_MESH_ATLASPACKER_H
|
||||
|
|
|
|||
37
thirdparty/thekla_atlas/thekla/thekla_atlas.cpp
vendored
37
thirdparty/thekla_atlas/thekla/thekla_atlas.cpp
vendored
|
|
@ -4,8 +4,8 @@
|
|||
#include <cfloat>
|
||||
|
||||
#include "nvmesh/halfedge/Edge.h"
|
||||
#include "nvmesh/halfedge/Face.h"
|
||||
#include "nvmesh/halfedge/Mesh.h"
|
||||
#include "nvmesh/halfedge/Face.h"
|
||||
#include "nvmesh/halfedge/Vertex.h"
|
||||
#include "nvmesh/param/Atlas.h"
|
||||
|
||||
|
|
@ -14,16 +14,18 @@
|
|||
|
||||
#include "nvcore/Array.inl"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace Thekla;
|
||||
using namespace nv;
|
||||
|
||||
|
||||
inline Atlas_Output_Mesh * set_error(Atlas_Error * error, Atlas_Error code) {
|
||||
if (error) *error = code;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void input_to_mesh(const Atlas_Input_Mesh * input, HalfEdge::Mesh * mesh, Atlas_Error * error) {
|
||||
|
||||
Array<uint> canonicalMap;
|
||||
|
|
@ -44,6 +46,7 @@ static void input_to_mesh(const Atlas_Input_Mesh *input, HalfEdge::Mesh *mesh, A
|
|||
|
||||
mesh->linkColocalsWithCanonicalMap(canonicalMap);
|
||||
|
||||
|
||||
const int face_count = input->face_count;
|
||||
|
||||
int non_manifold_faces = 0;
|
||||
|
|
@ -57,7 +60,8 @@ static void input_to_mesh(const Atlas_Input_Mesh *input, HalfEdge::Mesh *mesh, A
|
|||
HalfEdge::Face * face = mesh->addFace(v0, v1, v2);
|
||||
if (face != NULL) {
|
||||
face->material = input_face.material_index;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
non_manifold_faces++;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +112,6 @@ static Atlas_Output_Mesh *mesh_atlas_to_output(const HalfEdge::Mesh *mesh, const
|
|||
output->index_count = face_count * 3;
|
||||
output->index_array = new int[face_count * 3];
|
||||
|
||||
int face_ofs = 0;
|
||||
// Set face indices.
|
||||
for (int f = 0; f < face_count; f++) {
|
||||
uint c = charts->faceChartAt(f);
|
||||
|
|
@ -118,22 +121,14 @@ static Atlas_Output_Mesh *mesh_atlas_to_output(const HalfEdge::Mesh *mesh, const
|
|||
const Chart * chart = charts->chartAt(c);
|
||||
nvDebugCheck(chart->faceAt(i) == f);
|
||||
|
||||
if (i >= chart->chartMesh()->faceCount()) {
|
||||
printf("WARNING: Faces may be missing in the final vertex, which could not be packed\n");
|
||||
|
||||
continue;
|
||||
}
|
||||
const HalfEdge::Face * face = chart->chartMesh()->faceAt(i);
|
||||
const HalfEdge::Edge * edge = face->edge;
|
||||
|
||||
output->index_array[3 * face_ofs + 0] = vertexOffset + edge->vertex->id;
|
||||
output->index_array[3 * face_ofs + 1] = vertexOffset + edge->next->vertex->id;
|
||||
output->index_array[3 * face_ofs + 2] = vertexOffset + edge->next->next->vertex->id;
|
||||
face_ofs++;
|
||||
output->index_array[3*f+0] = vertexOffset + edge->vertex->id;
|
||||
output->index_array[3*f+1] = vertexOffset + edge->next->vertex->id;
|
||||
output->index_array[3*f+2] = vertexOffset + edge->next->next->vertex->id;
|
||||
}
|
||||
|
||||
output->index_count = face_ofs * 3;
|
||||
|
||||
*error = Atlas_Error_Success;
|
||||
output->atlas_width = w;
|
||||
output->atlas_height = h;
|
||||
|
|
@ -141,6 +136,7 @@ static Atlas_Output_Mesh *mesh_atlas_to_output(const HalfEdge::Mesh *mesh, const
|
|||
return output;
|
||||
}
|
||||
|
||||
|
||||
void Thekla::atlas_set_default_options(Atlas_Options * options) {
|
||||
if (options != NULL) {
|
||||
// These are the default values we use on The Witness.
|
||||
|
|
@ -164,6 +160,7 @@ void Thekla::atlas_set_default_options(Atlas_Options *options) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Atlas_Output_Mesh * Thekla::atlas_generate(const Atlas_Input_Mesh * input, const Atlas_Options * options, Atlas_Error * error) {
|
||||
// Validate args.
|
||||
if (input == NULL || options == NULL || error == NULL) return set_error(error, Atlas_Error_Invalid_Args);
|
||||
|
|
@ -198,11 +195,13 @@ Atlas_Output_Mesh *Thekla::atlas_generate(const Atlas_Input_Mesh *input, const A
|
|||
|
||||
if (v0 < 0 || v0 >= input->vertex_count ||
|
||||
v1 < 0 || v1 >= input->vertex_count ||
|
||||
v2 < 0 || v2 >= input->vertex_count) {
|
||||
v2 < 0 || v2 >= input->vertex_count)
|
||||
{
|
||||
return set_error(error, Atlas_Error_Invalid_Mesh);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Build half edge mesh.
|
||||
AutoPtr<HalfEdge::Mesh> mesh(new HalfEdge::Mesh);
|
||||
|
||||
|
|
@ -217,7 +216,8 @@ Atlas_Output_Mesh *Thekla::atlas_generate(const Atlas_Input_Mesh *input, const A
|
|||
// Charter.
|
||||
if (options->charter == Atlas_Charter_Extract) {
|
||||
return set_error(error, Atlas_Error_Not_Implemented);
|
||||
} else if (options->charter == Atlas_Charter_Witness) {
|
||||
}
|
||||
else if (options->charter == Atlas_Charter_Witness) {
|
||||
SegmentationSettings segmentation_settings;
|
||||
segmentation_settings.proxyFitMetricWeight = options->charter_options.witness.proxy_fit_metric_weight;
|
||||
segmentation_settings.roundnessMetricWeight = options->charter_options.witness.roundness_metric_weight;
|
||||
|
|
@ -255,10 +255,12 @@ Atlas_Output_Mesh *Thekla::atlas_generate(const Atlas_Input_Mesh *input, const A
|
|||
if (atlas.hasFailed())
|
||||
return NULL;
|
||||
|
||||
|
||||
// Build output mesh.
|
||||
return mesh_atlas_to_output(mesh.ptr(), atlas, error);
|
||||
}
|
||||
|
||||
|
||||
void Thekla::atlas_free(Atlas_Output_Mesh * output) {
|
||||
if (output != NULL) {
|
||||
delete [] output->vertex_array;
|
||||
|
|
@ -266,3 +268,4 @@ void Thekla::atlas_free(Atlas_Output_Mesh *output) {
|
|||
delete output;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue