Make uv unwrap faster
This commit is contained in:
@@ -5,11 +5,11 @@
|
||||
#include "iron_ui.h"
|
||||
#include "iron_ui_nodes.h"
|
||||
|
||||
void proc_xatlas_unwrap(void *mesh);
|
||||
FN(proc_xatlas_unwrap) {
|
||||
void proc_uv_unwrap(void *mesh);
|
||||
FN(proc_uv_unwrap) {
|
||||
uint64_t mesh;
|
||||
JS_ToBigUint64(ctx, &mesh, argv[0]);
|
||||
proc_xatlas_unwrap((void *)mesh);
|
||||
proc_uv_unwrap((void *)mesh);
|
||||
return JS_UNDEFINED;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ FN(path_texture_importers_delete) {
|
||||
void plugin_embed() {
|
||||
JSValue global_obj = JS_GetGlobalObject(js_ctx);
|
||||
|
||||
BIND(proc_xatlas_unwrap, 1);
|
||||
BIND(proc_uv_unwrap, 1);
|
||||
BIND(plugin_uv_unwrap_button, 0);
|
||||
BIND(io_svg_parse, 1);
|
||||
BIND(io_exr_parse, 1);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2020 Jonathan Young
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
https://github.com/jpcy/xatlas
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,269 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2020 Jonathan Young
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
/*
|
||||
thekla_atlas
|
||||
MIT License
|
||||
https://github.com/Thekla/thekla_atlas
|
||||
Copyright (c) 2013 Thekla, Inc
|
||||
Copyright NVIDIA Corporation 2006 -- Ignacio Castano <icastano@nvidia.com>
|
||||
*/
|
||||
#pragma once
|
||||
#ifndef XATLAS_H
|
||||
#define XATLAS_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace xatlas {
|
||||
|
||||
enum class ChartType
|
||||
{
|
||||
Planar,
|
||||
Ortho,
|
||||
LSCM,
|
||||
Piecewise,
|
||||
Invalid
|
||||
};
|
||||
|
||||
// A group of connected faces, belonging to a single atlas.
|
||||
struct Chart
|
||||
{
|
||||
uint32_t *faceArray;
|
||||
uint32_t atlasIndex; // Sub-atlas index.
|
||||
uint32_t faceCount;
|
||||
ChartType type;
|
||||
uint32_t material;
|
||||
};
|
||||
|
||||
// Output vertex.
|
||||
struct Vertex
|
||||
{
|
||||
int32_t atlasIndex; // Sub-atlas index. -1 if the vertex doesn't exist in any atlas.
|
||||
int32_t chartIndex; // -1 if the vertex doesn't exist in any chart.
|
||||
float uv[2]; // Not normalized - values are in Atlas width and height range.
|
||||
uint32_t xref; // Index of input vertex from which this output vertex originated.
|
||||
};
|
||||
|
||||
// Output mesh.
|
||||
struct Mesh
|
||||
{
|
||||
Chart *chartArray;
|
||||
uint32_t *indexArray;
|
||||
Vertex *vertexArray;
|
||||
uint32_t chartCount;
|
||||
uint32_t indexCount;
|
||||
uint32_t vertexCount;
|
||||
};
|
||||
|
||||
static const uint32_t kImageChartIndexMask = 0x1FFFFFFF;
|
||||
static const uint32_t kImageHasChartIndexBit = 0x80000000;
|
||||
static const uint32_t kImageIsBilinearBit = 0x40000000;
|
||||
static const uint32_t kImageIsPaddingBit = 0x20000000;
|
||||
|
||||
// Empty on creation. Populated after charts are packed.
|
||||
struct Atlas
|
||||
{
|
||||
uint32_t *image;
|
||||
Mesh *meshes; // The output meshes, corresponding to each AddMesh call.
|
||||
float *utilization; // Normalized atlas texel utilization array. E.g. a value of 0.8 means 20% empty space. atlasCount in length.
|
||||
uint32_t width; // Atlas width in texels.
|
||||
uint32_t height; // Atlas height in texels.
|
||||
uint32_t atlasCount; // Number of sub-atlases. Equal to 0 unless PackOptions resolution is changed from default (0).
|
||||
uint32_t chartCount; // Total number of charts in all meshes.
|
||||
uint32_t meshCount; // Number of output meshes. Equal to the number of times AddMesh was called.
|
||||
float texelsPerUnit; // Equal to PackOptions texelsPerUnit if texelsPerUnit > 0, otherwise an estimated value to match PackOptions resolution.
|
||||
};
|
||||
|
||||
// Create an empty atlas.
|
||||
Atlas *Create();
|
||||
|
||||
void Destroy(Atlas *atlas);
|
||||
|
||||
enum class IndexFormat
|
||||
{
|
||||
UInt16,
|
||||
UInt32
|
||||
};
|
||||
|
||||
// Input mesh declaration.
|
||||
struct MeshDecl
|
||||
{
|
||||
const void *vertexPositionData = nullptr;
|
||||
const void *vertexNormalData = nullptr; // optional
|
||||
const void *vertexUvData = nullptr; // optional. The input UVs are provided as a hint to the chart generator.
|
||||
const void *indexData = nullptr; // optional
|
||||
|
||||
// Optional. Must be faceCount in length.
|
||||
// Don't atlas faces set to true. Ignored faces still exist in the output meshes, Vertex uv is set to (0, 0) and Vertex atlasIndex to -1.
|
||||
const bool *faceIgnoreData = nullptr;
|
||||
|
||||
// Optional. Must be faceCount in length.
|
||||
// Only faces with the same material will be assigned to the same chart.
|
||||
const uint32_t *faceMaterialData = nullptr;
|
||||
|
||||
// Optional. Must be faceCount in length.
|
||||
// Polygon / n-gon support. Faces are assumed to be triangles if this is null.
|
||||
const uint8_t *faceVertexCount = nullptr;
|
||||
|
||||
uint32_t vertexCount = 0;
|
||||
uint32_t vertexPositionStride = 0;
|
||||
uint32_t vertexNormalStride = 0; // optional
|
||||
uint32_t vertexUvStride = 0; // optional
|
||||
uint32_t indexCount = 0;
|
||||
int32_t indexOffset = 0; // optional. Add this offset to all indices.
|
||||
uint32_t faceCount = 0; // Optional if faceVertexCount is null. Otherwise assumed to be indexCount / 3.
|
||||
IndexFormat indexFormat = IndexFormat::UInt16;
|
||||
|
||||
// Vertex positions within epsilon distance of each other are considered colocal.
|
||||
float epsilon = 1.192092896e-07F;
|
||||
};
|
||||
|
||||
enum class AddMeshError
|
||||
{
|
||||
Success, // No error.
|
||||
Error, // Unspecified error.
|
||||
IndexOutOfRange, // An index is >= MeshDecl vertexCount.
|
||||
InvalidFaceVertexCount, // Must be >= 3.
|
||||
InvalidIndexCount // Not evenly divisible by 3 - expecting triangles.
|
||||
};
|
||||
|
||||
// Add a mesh to the atlas. MeshDecl data is copied, so it can be freed after AddMesh returns.
|
||||
AddMeshError AddMesh(Atlas *atlas, const MeshDecl &meshDecl, uint32_t meshCountHint = 0);
|
||||
|
||||
// Wait for AddMesh async processing to finish. ComputeCharts / Generate call this internally.
|
||||
void AddMeshJoin(Atlas *atlas);
|
||||
|
||||
struct UvMeshDecl
|
||||
{
|
||||
const void *vertexUvData = nullptr;
|
||||
const void *indexData = nullptr; // optional
|
||||
const uint32_t *faceMaterialData = nullptr; // Optional. Overlapping UVs should be assigned a different material. Must be indexCount / 3 in length.
|
||||
uint32_t vertexCount = 0;
|
||||
uint32_t vertexStride = 0;
|
||||
uint32_t indexCount = 0;
|
||||
int32_t indexOffset = 0; // optional. Add this offset to all indices.
|
||||
IndexFormat indexFormat = IndexFormat::UInt16;
|
||||
};
|
||||
|
||||
AddMeshError AddUvMesh(Atlas *atlas, const UvMeshDecl &decl);
|
||||
|
||||
// Custom parameterization function. texcoords initial values are an orthogonal parameterization.
|
||||
typedef void (*ParameterizeFunc)(const float *positions, float *texcoords, uint32_t vertexCount, const uint32_t *indices, uint32_t indexCount);
|
||||
|
||||
struct ChartOptions
|
||||
{
|
||||
ParameterizeFunc paramFunc = nullptr;
|
||||
|
||||
float maxChartArea = 0.0f; // Don't grow charts to be larger than this. 0 means no limit.
|
||||
float maxBoundaryLength = 0.0f; // Don't grow charts to have a longer boundary than this. 0 means no limit.
|
||||
|
||||
// Weights determine chart growth. Higher weights mean higher cost for that metric.
|
||||
float normalDeviationWeight = 2.0f; // Angle between face and average chart normal.
|
||||
float roundnessWeight = 0.01f;
|
||||
float straightnessWeight = 6.0f;
|
||||
float normalSeamWeight = 4.0f; // If > 1000, normal seams are fully respected.
|
||||
float textureSeamWeight = 0.5f;
|
||||
|
||||
float maxCost = 2.0f; // If total of all metrics * weights > maxCost, don't grow chart. Lower values result in more charts.
|
||||
uint32_t maxIterations = 1; // Number of iterations of the chart growing and seeding phases. Higher values result in better charts.
|
||||
|
||||
bool useInputMeshUvs = false; // Use MeshDecl::vertexUvData for charts.
|
||||
bool fixWinding = false; // Enforce consistent texture coordinate winding.
|
||||
};
|
||||
|
||||
// Call after all AddMesh calls. Can be called multiple times to recompute charts with different options.
|
||||
void ComputeCharts(Atlas *atlas, ChartOptions options = ChartOptions());
|
||||
|
||||
struct PackOptions
|
||||
{
|
||||
// Charts larger than this will be scaled down. 0 means no limit.
|
||||
uint32_t maxChartSize = 0;
|
||||
|
||||
// Number of pixels to pad charts with.
|
||||
uint32_t padding = 0;
|
||||
|
||||
// Unit to texel scale. e.g. a 1x1 quad with texelsPerUnit of 32 will take up approximately 32x32 texels in the atlas.
|
||||
// If 0, an estimated value will be calculated to approximately match the given resolution.
|
||||
// If resolution is also 0, the estimated value will approximately match a 1024x1024 atlas.
|
||||
float texelsPerUnit = 0.0f;
|
||||
|
||||
// If 0, generate a single atlas with texelsPerUnit determining the final resolution.
|
||||
// If not 0, and texelsPerUnit is not 0, generate one or more atlases with that exact resolution.
|
||||
// If not 0, and texelsPerUnit is 0, texelsPerUnit is estimated to approximately match the resolution.
|
||||
uint32_t resolution = 0;
|
||||
|
||||
// Leave space around charts for texels that would be sampled by bilinear filtering.
|
||||
bool bilinear = true;
|
||||
|
||||
// Align charts to 4x4 blocks. Also improves packing speed, since there are fewer possible chart locations to consider.
|
||||
bool blockAlign = false;
|
||||
|
||||
// Slower, but gives the best result. If false, use random chart placement.
|
||||
bool bruteForce = false;
|
||||
|
||||
// Create Atlas::image
|
||||
bool createImage = false;
|
||||
|
||||
// Rotate charts to the axis of their convex hull.
|
||||
bool rotateChartsToAxis = true;
|
||||
|
||||
// Rotate charts to improve packing.
|
||||
bool rotateCharts = true;
|
||||
};
|
||||
|
||||
// Call after ComputeCharts. Can be called multiple times to re-pack charts with different options.
|
||||
void PackCharts(Atlas *atlas, PackOptions packOptions = PackOptions());
|
||||
|
||||
// Equivalent to calling ComputeCharts and PackCharts in sequence. Can be called multiple times to regenerate with different options.
|
||||
void Generate(Atlas *atlas, ChartOptions chartOptions = ChartOptions(), PackOptions packOptions = PackOptions());
|
||||
|
||||
// Progress tracking.
|
||||
enum class ProgressCategory
|
||||
{
|
||||
AddMesh,
|
||||
ComputeCharts,
|
||||
PackCharts,
|
||||
BuildOutputMeshes
|
||||
};
|
||||
|
||||
// May be called from any thread. Return false to cancel.
|
||||
typedef bool (*ProgressFunc)(ProgressCategory category, int progress, void *userData);
|
||||
|
||||
void SetProgressCallback(Atlas *atlas, ProgressFunc progressFunc = nullptr, void *progressUserData = nullptr);
|
||||
|
||||
// Custom memory allocation.
|
||||
typedef void *(*ReallocFunc)(void *, size_t);
|
||||
typedef void (*FreeFunc)(void *);
|
||||
void SetAlloc(ReallocFunc reallocFunc, FreeFunc freeFunc = nullptr);
|
||||
|
||||
// Custom print function.
|
||||
typedef int (*PrintFunc)(const char *, ...);
|
||||
void SetPrint(PrintFunc print, bool verbose);
|
||||
|
||||
// Helper functions for error messages.
|
||||
const char *StringForEnum(AddMeshError error);
|
||||
const char *StringForEnum(ProgressCategory category);
|
||||
|
||||
} // namespace xatlas
|
||||
|
||||
#endif // XATLAS_H
|
||||
@@ -1,7 +1,7 @@
|
||||
let project = new Project("plugins");
|
||||
|
||||
project.add_cfiles("plugins.c");
|
||||
project.add_cfiles("proc_xatlas/**");
|
||||
project.add_cfiles("uv_unwrap/**");
|
||||
project.add_cfiles("io_svg/**");
|
||||
project.add_cfiles("io_exr/**");
|
||||
project.add_cfiles("io_usd/**");
|
||||
|
||||
@@ -0,0 +1,694 @@
|
||||
|
||||
#include "uv_unwrap.h"
|
||||
#include <float.h>
|
||||
#include <iron_system.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
float x, y, z;
|
||||
} vec3_t;
|
||||
|
||||
typedef struct {
|
||||
float x, y;
|
||||
} vec2_t;
|
||||
|
||||
static inline vec3_t v3_add(vec3_t a, vec3_t b) {
|
||||
return (vec3_t){a.x + b.x, a.y + b.y, a.z + b.z};
|
||||
}
|
||||
static inline vec3_t v3_sub(vec3_t a, vec3_t b) {
|
||||
return (vec3_t){a.x - b.x, a.y - b.y, a.z - b.z};
|
||||
}
|
||||
static inline vec3_t v3_mul(vec3_t a, float s) {
|
||||
return (vec3_t){a.x * s, a.y * s, a.z * s};
|
||||
}
|
||||
static inline float v3_dot(vec3_t a, vec3_t b) {
|
||||
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||
}
|
||||
static inline float v3_len(vec3_t a) {
|
||||
return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z);
|
||||
}
|
||||
static inline vec3_t v3_cross(vec3_t a, vec3_t b) {
|
||||
return (vec3_t){a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
|
||||
}
|
||||
static inline vec3_t v3_norm(vec3_t a) {
|
||||
float l = v3_len(a);
|
||||
if (l < 1e-6f)
|
||||
return (vec3_t){0, 0, 1};
|
||||
return v3_mul(a, 1.0f / l);
|
||||
}
|
||||
|
||||
#define sb__raw(a) ((int *)(a) - 2)
|
||||
#define sb__len(a) (sb__raw(a)[0])
|
||||
#define sb__cap_val(a) (sb__raw(a)[1])
|
||||
#define sb_count(a) ((a) ? sb__len(a) : 0)
|
||||
#define sb_cap(a) ((a) ? sb__cap_val(a) : 0)
|
||||
#define sb_free(a) ((a) ? free(sb__raw(a)), 0 : 0)
|
||||
#define sb_push(a, v) (sb__grow((void **)&(a), sizeof(*(a))) ? ((a)[sb__len(a)++] = (v), 0) : 0)
|
||||
|
||||
static int sb__grow(void **ptr, size_t item_size) {
|
||||
int *arr = *ptr ? sb__raw(*ptr) : NULL;
|
||||
if (arr) {
|
||||
int len = arr[0];
|
||||
int cap = arr[1];
|
||||
if (len < cap)
|
||||
return 1;
|
||||
int new_cap = cap * 2;
|
||||
int *new_arr = (int *)realloc(arr, sizeof(int) * 2 + item_size * new_cap);
|
||||
if (!new_arr)
|
||||
return 0;
|
||||
new_arr[1] = new_cap;
|
||||
*ptr = new_arr + 2;
|
||||
}
|
||||
else {
|
||||
int new_cap = 16;
|
||||
int *new_arr = (int *)malloc(sizeof(int) * 2 + item_size * new_cap);
|
||||
if (!new_arr)
|
||||
return 0;
|
||||
new_arr[0] = 0;
|
||||
new_arr[1] = new_cap;
|
||||
*ptr = new_arr + 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int *faces; // SB
|
||||
vec2_t *uvs; // SB
|
||||
vec2_t min, max;
|
||||
int p_x, p_y;
|
||||
} Island;
|
||||
|
||||
typedef struct {
|
||||
float *pa;
|
||||
int index;
|
||||
} sort_vert_t;
|
||||
|
||||
typedef struct {
|
||||
int u1, u2, face;
|
||||
} edge_ref_t;
|
||||
|
||||
typedef struct {
|
||||
int spatial_idx;
|
||||
int local_idx;
|
||||
} relax_vert_t;
|
||||
|
||||
typedef struct {
|
||||
int u, v;
|
||||
} relax_edge_t;
|
||||
|
||||
typedef struct pack_node {
|
||||
struct pack_node *child[2];
|
||||
int x, y, w, h;
|
||||
int occupied;
|
||||
} pack_node_t;
|
||||
|
||||
static int compare_verts(const void *a, const void *b) {
|
||||
const sort_vert_t *va = (const sort_vert_t *)a;
|
||||
const sort_vert_t *vb = (const sort_vert_t *)b;
|
||||
float eps = 0.0001f;
|
||||
if (fabsf(va->pa[0] - vb->pa[0]) > eps)
|
||||
return (va->pa[0] > vb->pa[0]) ? 1 : -1;
|
||||
if (fabsf(va->pa[1] - vb->pa[1]) > eps)
|
||||
return (va->pa[1] > vb->pa[1]) ? 1 : -1;
|
||||
if (fabsf(va->pa[2] - vb->pa[2]) > eps)
|
||||
return (va->pa[2] > vb->pa[2]) ? 1 : -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int compare_edges(const void *a, const void *b) {
|
||||
const edge_ref_t *ea = (const edge_ref_t *)a;
|
||||
const edge_ref_t *eb = (const edge_ref_t *)b;
|
||||
if (ea->u1 != eb->u1)
|
||||
return ea->u1 - eb->u1;
|
||||
return ea->u2 - eb->u2;
|
||||
}
|
||||
|
||||
static int compare_relax_verts(const void *a, const void *b) {
|
||||
const relax_vert_t *va = (const relax_vert_t *)a;
|
||||
const relax_vert_t *vb = (const relax_vert_t *)b;
|
||||
return va->spatial_idx - vb->spatial_idx;
|
||||
}
|
||||
|
||||
static int compare_relax_edges(const void *a, const void *b) {
|
||||
const relax_edge_t *ea = (const relax_edge_t *)a;
|
||||
const relax_edge_t *eb = (const relax_edge_t *)b;
|
||||
if (ea->u != eb->u)
|
||||
return ea->u - eb->u;
|
||||
return ea->v - eb->v;
|
||||
}
|
||||
|
||||
static int compare_islands_area(const void *a, const void *b) {
|
||||
const Island *ia = (const Island *)a;
|
||||
const Island *ib = (const Island *)b;
|
||||
float area_a = (ia->max.x - ia->min.x) * (ia->max.y - ia->min.y);
|
||||
float area_b = (ib->max.x - ib->min.x) * (ib->max.y - ib->min.y);
|
||||
return (area_a < area_b) ? 1 : -1;
|
||||
}
|
||||
|
||||
static pack_node_t *pack_insert(pack_node_t *node, int w, int h) {
|
||||
if (!node)
|
||||
return NULL;
|
||||
|
||||
if (w > node->w || h > node->h)
|
||||
return NULL;
|
||||
|
||||
if (!node->child[0]) {
|
||||
if (node->occupied)
|
||||
return NULL;
|
||||
|
||||
// Perfect fit or fits within node
|
||||
// If perfectly same size, use it
|
||||
if (w == node->w && h == node->h) {
|
||||
node->occupied = 1;
|
||||
return node;
|
||||
}
|
||||
|
||||
// Split the node to create a perfect fit for the requested size
|
||||
node->child[0] = (pack_node_t *)calloc(1, sizeof(pack_node_t));
|
||||
node->child[1] = (pack_node_t *)calloc(1, sizeof(pack_node_t));
|
||||
|
||||
// Decide split axis based on maximizing the remaining rectangle area
|
||||
int dw = node->w - w;
|
||||
int dh = node->h - h;
|
||||
|
||||
if (dw > dh) {
|
||||
// Split vertically: Child 0 is Left (fit w), Child 1 is Right (remainder)
|
||||
node->child[0]->x = node->x;
|
||||
node->child[0]->y = node->y;
|
||||
node->child[0]->w = w;
|
||||
node->child[0]->h = node->h; // Keep full height
|
||||
|
||||
node->child[1]->x = node->x + w;
|
||||
node->child[1]->y = node->y;
|
||||
node->child[1]->w = dw;
|
||||
node->child[1]->h = node->h;
|
||||
}
|
||||
else {
|
||||
// Split horizontally: Child 0 is Top (fit h), Child 1 is Bottom (remainder)
|
||||
node->child[0]->x = node->x;
|
||||
node->child[0]->y = node->y;
|
||||
node->child[0]->w = node->w; // Keep full width
|
||||
node->child[0]->h = h;
|
||||
|
||||
node->child[1]->x = node->x;
|
||||
node->child[1]->y = node->y + h;
|
||||
node->child[1]->w = node->w;
|
||||
node->child[1]->h = dh;
|
||||
}
|
||||
|
||||
// Insert into the newly created child that matches the dimension we just set up
|
||||
return pack_insert(node->child[0], w, h);
|
||||
}
|
||||
|
||||
// Recursive insert
|
||||
pack_node_t *res = pack_insert(node->child[0], w, h);
|
||||
if (!res)
|
||||
res = pack_insert(node->child[1], w, h);
|
||||
return res;
|
||||
}
|
||||
|
||||
static void free_pack_node(pack_node_t *node) {
|
||||
if (!node)
|
||||
return;
|
||||
free_pack_node(node->child[0]);
|
||||
free_pack_node(node->child[1]);
|
||||
free(node);
|
||||
}
|
||||
|
||||
static void relax_island(Island *isl, uint32_t *indices, float *pa, int iterations) {
|
||||
if (sb_count(isl->faces) < 2)
|
||||
return;
|
||||
|
||||
int num_uvs = sb_count(isl->uvs);
|
||||
|
||||
relax_vert_t *rverts = (relax_vert_t *)malloc(sizeof(relax_vert_t) * num_uvs);
|
||||
for (int i = 0; i < num_uvs; ++i) {
|
||||
int f = i / 3;
|
||||
int v = i % 3;
|
||||
rverts[i].spatial_idx = indices[isl->faces[f] * 3 + v];
|
||||
rverts[i].local_idx = i;
|
||||
}
|
||||
qsort(rverts, num_uvs, sizeof(relax_vert_t), compare_relax_verts);
|
||||
|
||||
int *uv_weld_map = (int *)malloc(sizeof(int) * num_uvs);
|
||||
int current_group_id = -1;
|
||||
int last_spatial = -1;
|
||||
for (int i = 0; i < num_uvs; ++i) {
|
||||
if (rverts[i].spatial_idx != last_spatial) {
|
||||
current_group_id = rverts[i].local_idx;
|
||||
last_spatial = rverts[i].spatial_idx;
|
||||
}
|
||||
uv_weld_map[rverts[i].local_idx] = current_group_id;
|
||||
}
|
||||
free(rverts);
|
||||
|
||||
relax_edge_t *redges = NULL;
|
||||
typedef struct {
|
||||
int count;
|
||||
int neighbors[8];
|
||||
float dists[8];
|
||||
} VInfo;
|
||||
VInfo *v_info = (VInfo *)calloc(num_uvs, sizeof(VInfo));
|
||||
|
||||
for (int f = 0; f < sb_count(isl->faces); ++f) {
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
int u_local = f * 3 + k;
|
||||
int v_local = f * 3 + (k + 1) % 3;
|
||||
int u_weld = uv_weld_map[u_local];
|
||||
int v_weld = uv_weld_map[v_local];
|
||||
if (u_weld > v_weld) {
|
||||
int t = u_weld;
|
||||
u_weld = v_weld;
|
||||
v_weld = t;
|
||||
}
|
||||
|
||||
relax_edge_t re = {u_weld, v_weld};
|
||||
sb_push(redges, re);
|
||||
|
||||
int idx_u = indices[isl->faces[f] * 3 + k];
|
||||
int idx_v = indices[isl->faces[f] * 3 + (k + 1) % 3];
|
||||
vec3_t p1 = {pa[idx_u * 3], pa[idx_u * 3 + 1], pa[idx_u * 3 + 2]};
|
||||
vec3_t p2 = {pa[idx_v * 3], pa[idx_v * 3 + 1], pa[idx_v * 3 + 2]};
|
||||
float d = v3_len(v3_sub(p1, p2));
|
||||
|
||||
if (v_info[u_local].count < 8) {
|
||||
v_info[u_local].neighbors[v_info[u_local].count] = v_local;
|
||||
v_info[u_local].dists[v_info[u_local].count] = d;
|
||||
v_info[u_local].count++;
|
||||
}
|
||||
if (v_info[v_local].count < 8) {
|
||||
v_info[v_local].neighbors[v_info[v_local].count] = u_local;
|
||||
v_info[v_local].dists[v_info[v_local].count] = d;
|
||||
v_info[v_local].count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qsort(redges, sb_count(redges), sizeof(relax_edge_t), compare_relax_edges);
|
||||
|
||||
char *weld_is_border = (char *)calloc(num_uvs, 1);
|
||||
int num_edges = sb_count(redges);
|
||||
int i = 0;
|
||||
while (i < num_edges) {
|
||||
int count = 1;
|
||||
while (i + count < num_edges && redges[i + count].u == redges[i].u && redges[i + count].v == redges[i].v)
|
||||
count++;
|
||||
if (count == 1) {
|
||||
weld_is_border[redges[i].u] = 1;
|
||||
weld_is_border[redges[i].v] = 1;
|
||||
}
|
||||
i += count;
|
||||
}
|
||||
sb_free(redges);
|
||||
|
||||
char *is_border = (char *)calloc(num_uvs, 1);
|
||||
for (int k = 0; k < num_uvs; ++k) {
|
||||
if (weld_is_border[uv_weld_map[k]])
|
||||
is_border[k] = 1;
|
||||
}
|
||||
free(weld_is_border);
|
||||
|
||||
vec2_t *temp_uvs = (vec2_t *)malloc(sizeof(vec2_t) * num_uvs);
|
||||
float *sum_x = (float *)calloc(num_uvs, sizeof(float));
|
||||
float *sum_y = (float *)calloc(num_uvs, sizeof(float));
|
||||
int *counts = (int *)calloc(num_uvs, sizeof(int));
|
||||
|
||||
for (int iter = 0; iter < iterations; ++iter) {
|
||||
memcpy(temp_uvs, isl->uvs, sizeof(vec2_t) * num_uvs);
|
||||
|
||||
for (int k = 0; k < num_uvs; ++k) {
|
||||
if (is_border[k])
|
||||
continue;
|
||||
|
||||
vec2_t sum_pos = {0, 0};
|
||||
float weight_sum = 0;
|
||||
|
||||
for (int n = 0; n < v_info[k].count; ++n) {
|
||||
int neighbor_idx = v_info[k].neighbors[n];
|
||||
float target_dist = v_info[k].dists[n];
|
||||
if (target_dist < 0.0001f)
|
||||
target_dist = 0.0001f;
|
||||
|
||||
vec2_t n_pos = temp_uvs[neighbor_idx];
|
||||
float w = 1.0f / target_dist;
|
||||
|
||||
sum_pos.x += n_pos.x * w;
|
||||
sum_pos.y += n_pos.y * w;
|
||||
weight_sum += w;
|
||||
}
|
||||
|
||||
if (weight_sum > 0) {
|
||||
isl->uvs[k].x = sum_pos.x / weight_sum;
|
||||
isl->uvs[k].y = sum_pos.y / weight_sum;
|
||||
}
|
||||
}
|
||||
|
||||
memset(sum_x, 0, sizeof(float) * num_uvs);
|
||||
memset(sum_y, 0, sizeof(float) * num_uvs);
|
||||
memset(counts, 0, sizeof(int) * num_uvs);
|
||||
|
||||
for (int k = 0; k < num_uvs; ++k) {
|
||||
int wid = uv_weld_map[k];
|
||||
sum_x[wid] += isl->uvs[k].x;
|
||||
sum_y[wid] += isl->uvs[k].y;
|
||||
counts[wid]++;
|
||||
}
|
||||
|
||||
for (int k = 0; k < num_uvs; ++k) {
|
||||
int wid = uv_weld_map[k];
|
||||
if (counts[wid] > 1) {
|
||||
isl->uvs[k].x = sum_x[wid] / counts[wid];
|
||||
isl->uvs[k].y = sum_y[wid] / counts[wid];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(sum_x);
|
||||
free(sum_y);
|
||||
free(counts);
|
||||
free(temp_uvs);
|
||||
free(uv_weld_map);
|
||||
free(v_info);
|
||||
free(is_border);
|
||||
}
|
||||
|
||||
void proc_uv_unwrap(raw_mesh_t *mesh) {
|
||||
double t = iron_time();
|
||||
|
||||
// Prepare Data
|
||||
int vertex_count = mesh->posa->length / 4;
|
||||
float *pa = (float *)malloc(sizeof(float) * vertex_count * 3);
|
||||
float *na = (float *)malloc(sizeof(float) * vertex_count * 3);
|
||||
float inv = 1.0f / 32767.0f;
|
||||
|
||||
for (int i = 0; i < vertex_count; i++) {
|
||||
pa[i * 3] = mesh->posa->buffer[i * 4] * inv;
|
||||
pa[i * 3 + 1] = mesh->posa->buffer[i * 4 + 1] * inv;
|
||||
pa[i * 3 + 2] = mesh->posa->buffer[i * 4 + 2] * inv;
|
||||
na[i * 3] = mesh->nora->buffer[i * 2] * inv;
|
||||
na[i * 3 + 1] = mesh->nora->buffer[i * 2 + 1] * inv;
|
||||
na[i * 3 + 2] = mesh->posa->buffer[i * 4 + 3] * inv;
|
||||
}
|
||||
|
||||
int index_count = mesh->inda->length;
|
||||
uint32_t *indices = mesh->inda->buffer;
|
||||
int face_count = index_count / 3;
|
||||
|
||||
// Weld
|
||||
int *weld_map = (int *)malloc(sizeof(int) * vertex_count);
|
||||
sort_vert_t *sverts = (sort_vert_t *)malloc(sizeof(sort_vert_t) * vertex_count);
|
||||
for (int i = 0; i < vertex_count; i++) {
|
||||
sverts[i].pa = &pa[i * 3];
|
||||
sverts[i].index = i;
|
||||
}
|
||||
qsort(sverts, vertex_count, sizeof(sort_vert_t), compare_verts);
|
||||
|
||||
int unique_counter = 0;
|
||||
float eps = 0.0001f;
|
||||
for (int i = 0; i < vertex_count; i++) {
|
||||
if (i > 0) {
|
||||
if (fabsf(sverts[i].pa[0] - sverts[i - 1].pa[0]) > eps || fabsf(sverts[i].pa[1] - sverts[i - 1].pa[1]) > eps ||
|
||||
fabsf(sverts[i].pa[2] - sverts[i - 1].pa[2]) > eps) {
|
||||
unique_counter++;
|
||||
}
|
||||
}
|
||||
weld_map[sverts[i].index] = unique_counter;
|
||||
}
|
||||
free(sverts);
|
||||
|
||||
// Adjacency
|
||||
vec3_t *face_normals = (vec3_t *)malloc(sizeof(vec3_t) * face_count);
|
||||
for (int i = 0; i < face_count; i++) {
|
||||
uint32_t i0 = indices[i * 3];
|
||||
uint32_t i1 = indices[i * 3 + 1];
|
||||
uint32_t i2 = indices[i * 3 + 2];
|
||||
vec3_t v0 = {pa[i0 * 3], pa[i0 * 3 + 1], pa[i0 * 3 + 2]};
|
||||
vec3_t v1 = {pa[i1 * 3], pa[i1 * 3 + 1], pa[i1 * 3 + 2]};
|
||||
vec3_t v2 = {pa[i2 * 3], pa[i2 * 3 + 1], pa[i2 * 3 + 2]};
|
||||
face_normals[i] = v3_norm(v3_cross(v3_sub(v1, v0), v3_sub(v2, v0)));
|
||||
}
|
||||
|
||||
edge_ref_t *edges = (edge_ref_t *)malloc(sizeof(edge_ref_t) * face_count * 3);
|
||||
for (int i = 0; i < face_count; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
int idx1 = weld_map[indices[i * 3 + j]];
|
||||
int idx2 = weld_map[indices[i * 3 + (j + 1) % 3]];
|
||||
if (idx1 > idx2) {
|
||||
int t = idx1;
|
||||
idx1 = idx2;
|
||||
idx2 = t;
|
||||
}
|
||||
edges[i * 3 + j] = (edge_ref_t){idx1, idx2, i};
|
||||
}
|
||||
}
|
||||
qsort(edges, face_count * 3, sizeof(edge_ref_t), compare_edges);
|
||||
|
||||
int **adj = (int **)calloc(face_count, sizeof(int *));
|
||||
for (int i = 0; i < face_count * 3 - 1; i++) {
|
||||
if (edges[i].u1 == edges[i + 1].u1 && edges[i].u2 == edges[i + 1].u2) {
|
||||
int f1 = edges[i].face;
|
||||
int f2 = edges[i + 1].face;
|
||||
sb_push(adj[f1], f2);
|
||||
sb_push(adj[f2], f1);
|
||||
}
|
||||
}
|
||||
free(edges);
|
||||
|
||||
// Segmentation
|
||||
float angle_limit_deg = 66.0f;
|
||||
float angle_threshold = cosf(angle_limit_deg * (3.14159f / 180.0f));
|
||||
int *face_visited = (int *)calloc(face_count, sizeof(int));
|
||||
Island *islands = NULL;
|
||||
|
||||
for (int i = 0; i < face_count; i++) {
|
||||
if (face_visited[i])
|
||||
continue;
|
||||
|
||||
Island island = {0};
|
||||
int *queue = NULL;
|
||||
sb_push(queue, i);
|
||||
face_visited[i] = 1;
|
||||
sb_push(island.faces, i);
|
||||
|
||||
int head = 0;
|
||||
vec3_t island_avg = face_normals[i];
|
||||
|
||||
while (head < sb_count(queue)) {
|
||||
int curr_f = queue[head++];
|
||||
int *neighbors = adj[curr_f];
|
||||
for (int k = 0; k < sb_count(neighbors); k++) {
|
||||
int next_f = neighbors[k];
|
||||
if (face_visited[next_f])
|
||||
continue;
|
||||
|
||||
float dot_local = v3_dot(face_normals[curr_f], face_normals[next_f]);
|
||||
float dot_global = v3_dot(v3_norm(island_avg), face_normals[next_f]);
|
||||
|
||||
if (dot_local > angle_threshold && dot_global > 0.5f) {
|
||||
face_visited[next_f] = 1;
|
||||
sb_push(island.faces, next_f);
|
||||
sb_push(queue, next_f);
|
||||
island_avg = v3_add(island_avg, face_normals[next_f]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Project
|
||||
island_avg = v3_norm(island_avg);
|
||||
vec3_t up = {0, 1, 0};
|
||||
if (fabsf(v3_dot(up, island_avg)) > 0.9f)
|
||||
up = (vec3_t){0, 0, 1};
|
||||
vec3_t right = v3_norm(v3_cross(up, island_avg));
|
||||
up = v3_norm(v3_cross(island_avg, right));
|
||||
|
||||
for (int k = 0; k < sb_count(island.faces); k++) {
|
||||
int f = island.faces[k];
|
||||
for (int v = 0; v < 3; v++) {
|
||||
int idx = indices[f * 3 + v];
|
||||
vec3_t pos = {pa[idx * 3], pa[idx * 3 + 1], pa[idx * 3 + 2]};
|
||||
vec2_t uv;
|
||||
uv.x = v3_dot(pos, right);
|
||||
uv.y = v3_dot(pos, up);
|
||||
sb_push(island.uvs, uv);
|
||||
}
|
||||
}
|
||||
|
||||
// Relax
|
||||
relax_island(&island, indices, pa, 5);
|
||||
|
||||
// Calc Bounds
|
||||
island.min = (vec2_t){FLT_MAX, FLT_MAX};
|
||||
island.max = (vec2_t){-FLT_MAX, -FLT_MAX};
|
||||
for (int k = 0; k < sb_count(island.uvs); k++) {
|
||||
if (island.uvs[k].x < island.min.x)
|
||||
island.min.x = island.uvs[k].x;
|
||||
if (island.uvs[k].y < island.min.y)
|
||||
island.min.y = island.uvs[k].y;
|
||||
if (island.uvs[k].x > island.max.x)
|
||||
island.max.x = island.uvs[k].x;
|
||||
if (island.uvs[k].y > island.max.y)
|
||||
island.max.y = island.uvs[k].y;
|
||||
}
|
||||
|
||||
// Normalize to local 0,0
|
||||
for (int k = 0; k < sb_count(island.uvs); k++) {
|
||||
island.uvs[k].x -= island.min.x;
|
||||
island.uvs[k].y -= island.min.y;
|
||||
}
|
||||
|
||||
sb_push(islands, island);
|
||||
sb_free(queue);
|
||||
}
|
||||
|
||||
free(face_visited);
|
||||
for (int i = 0; i < face_count; i++)
|
||||
sb_free(adj[i]);
|
||||
free(adj);
|
||||
free(weld_map);
|
||||
|
||||
// Sort by Area
|
||||
qsort(islands, sb_count(islands), sizeof(Island), compare_islands_area);
|
||||
|
||||
int map_size = 2048;
|
||||
int padding = 4;
|
||||
|
||||
// Calc total area
|
||||
float total_area = 0;
|
||||
for (int i = 0; i < sb_count(islands); i++) {
|
||||
total_area += (islands[i].max.x - islands[i].min.x) * (islands[i].max.y - islands[i].min.y);
|
||||
}
|
||||
|
||||
// Iterative Fit
|
||||
float ideal_scale = 1.0f;
|
||||
if (total_area > 0.0001f) {
|
||||
ideal_scale = sqrtf((map_size * map_size * 1.0f) / total_area); // Start high
|
||||
}
|
||||
if (ideal_scale > 1000.0f)
|
||||
ideal_scale = 1000.0f;
|
||||
|
||||
float final_scale = 0;
|
||||
|
||||
// Retry loop
|
||||
for (float try_scale = ideal_scale; try_scale > 0.001f; try_scale *= 0.95f) {
|
||||
pack_node_t *root = (pack_node_t *)calloc(1, sizeof(pack_node_t));
|
||||
root->w = map_size;
|
||||
root->h = map_size;
|
||||
|
||||
int success = 1;
|
||||
for (int i = 0; i < sb_count(islands); i++) {
|
||||
int w = (int)((islands[i].max.x - islands[i].min.x) * try_scale) + padding * 2;
|
||||
int h = (int)((islands[i].max.y - islands[i].min.y) * try_scale) + padding * 2;
|
||||
if (w < 1)
|
||||
w = 1;
|
||||
if (h < 1)
|
||||
h = 1;
|
||||
|
||||
pack_node_t *node = pack_insert(root, w, h);
|
||||
if (node) {
|
||||
// Store temporary packed coords in island
|
||||
islands[i].p_x = node->x;
|
||||
islands[i].p_y = node->y;
|
||||
}
|
||||
else {
|
||||
success = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free_pack_node(root);
|
||||
|
||||
if (success) {
|
||||
final_scale = try_scale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Final Apply
|
||||
for (int i = 0; i < sb_count(islands); i++) {
|
||||
float off_x = (islands[i].p_x + padding) / (float)map_size;
|
||||
float off_y = (islands[i].p_y + padding) / (float)map_size;
|
||||
|
||||
// Recalculate dimensions based on the winning scale
|
||||
int w_px = (int)((islands[i].max.x - islands[i].min.x) * final_scale) + padding * 2;
|
||||
int h_px = (int)((islands[i].max.y - islands[i].min.y) * final_scale) + padding * 2;
|
||||
|
||||
float uv_w = (w_px - padding * 2) / (float)map_size;
|
||||
float uv_h = (h_px - padding * 2) / (float)map_size;
|
||||
|
||||
float local_w = (islands[i].max.x - islands[i].min.x);
|
||||
float local_h = (islands[i].max.y - islands[i].min.y);
|
||||
|
||||
for (int k = 0; k < sb_count(islands[i].uvs); k++) {
|
||||
float u_norm = (local_w > 1e-6f) ? islands[i].uvs[k].x / local_w : 0.0f;
|
||||
float v_norm = (local_h > 1e-6f) ? islands[i].uvs[k].y / local_h : 0.0f;
|
||||
islands[i].uvs[k].x = off_x + u_norm * uv_w;
|
||||
islands[i].uvs[k].y = off_y + v_norm * uv_h;
|
||||
}
|
||||
}
|
||||
|
||||
// Output
|
||||
int16_t *pa_out = NULL;
|
||||
int16_t *na_out = NULL;
|
||||
int16_t *ta_out = NULL;
|
||||
uint32_t *ia_out = NULL;
|
||||
int out_v_count = 0;
|
||||
|
||||
for (int i = 0; i < sb_count(islands); i++) {
|
||||
Island *isl = &islands[i];
|
||||
for (int f = 0; f < sb_count(isl->faces); f++) {
|
||||
int face_idx = isl->faces[f];
|
||||
for (int v = 0; v < 3; v++) {
|
||||
uint32_t old_idx = indices[face_idx * 3 + v];
|
||||
sb_push(pa_out, (int16_t)(pa[old_idx * 3] / inv));
|
||||
sb_push(pa_out, (int16_t)(pa[old_idx * 3 + 1] / inv));
|
||||
sb_push(pa_out, (int16_t)(pa[old_idx * 3 + 2] / inv));
|
||||
sb_push(pa_out, (int16_t)(na[old_idx * 3 + 2] / inv));
|
||||
sb_push(na_out, (int16_t)(na[old_idx * 3] / inv));
|
||||
sb_push(na_out, (int16_t)(na[old_idx * 3 + 1] / inv));
|
||||
vec2_t uv = isl->uvs[f * 3 + v];
|
||||
sb_push(ta_out, (int16_t)(uv.x / inv));
|
||||
sb_push(ta_out, (int16_t)(uv.y / inv));
|
||||
sb_push(ia_out, out_v_count);
|
||||
out_v_count++;
|
||||
}
|
||||
}
|
||||
sb_free(isl->faces);
|
||||
sb_free(isl->uvs);
|
||||
}
|
||||
sb_free(islands);
|
||||
|
||||
if (mesh->posa->buffer)
|
||||
free(mesh->posa->buffer);
|
||||
if (mesh->nora->buffer)
|
||||
free(mesh->nora->buffer);
|
||||
if (mesh->inda->buffer)
|
||||
free(mesh->inda->buffer);
|
||||
if (mesh->texa) {
|
||||
if (mesh->texa->buffer)
|
||||
free(mesh->texa->buffer);
|
||||
free(mesh->texa);
|
||||
}
|
||||
|
||||
mesh->posa->buffer = pa_out;
|
||||
mesh->posa->length = out_v_count * 4;
|
||||
mesh->posa->capacity = sb_cap(pa_out);
|
||||
mesh->nora->buffer = na_out;
|
||||
mesh->nora->length = out_v_count * 2;
|
||||
mesh->texa = (i16_array_t *)malloc(sizeof(i16_array_t));
|
||||
mesh->texa->buffer = ta_out;
|
||||
mesh->texa->length = out_v_count * 2;
|
||||
mesh->texa->capacity = sb_cap(ta_out);
|
||||
mesh->inda->buffer = ia_out;
|
||||
mesh->inda->length = out_v_count;
|
||||
mesh->inda->capacity = sb_cap(ia_out);
|
||||
mesh->vertex_count = out_v_count;
|
||||
mesh->index_count = out_v_count;
|
||||
free(pa);
|
||||
free(na);
|
||||
free(face_normals);
|
||||
iron_log("Unwrapped in %fs\n", iron_time() - t);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <iron_obj.h>
|
||||
|
||||
void proc_uv_unwrap(raw_mesh_t *mesh);
|
||||
Reference in New Issue
Block a user