Texture Painting: Fix Seam Bleeding of Non-Manifold Sections of Mesh

Fix seam bleeding of non-manifold sections of the mesh, by copying pixels
that are covered by the brush stroke.

As manifold parts are already handled, the pixel copying solution can be
very straight forward.

* Pixels are copied from the same tile. So we don't need a mechanism that
  copies and merges pixels from other tiles.
* Pixels are copied from the closest pixel that is being painted on. We
  don't need to consider that that pixel can be in different areas of the
  tile.

When we copy a pixel, we find the closest pixel in UV space that is being
directly influenced by a paint brush. We also look for the second closest
pixel, which is still a neighbor from the closest pixel. We can mix both
pixels together and store it in the destination. A mix factor is calculated
using the closest non manifold edge as a guidance.

The result of this step is a list of copy and mix commands that can be
executed to fix the seam bleeding for non-manifold sections of the mesh.

| Destination | Source 1 | Source 2 | Mix factor |
| ----------- | -------- | -------- | ---------- |
| 1780,1811   | 1780,1810| 1779,1810| 0.000000   |
| 1781,1811   | 1781,1810| 1782,1811| 0.168627   |
| 1828,1811   | 1828,1810| 1827,1811| 0.156863   |
| 1829,1811   | 1829,1810| 1828,1810| 0.188235   |
| 1830,1811   | 1830,1810| 1829,1810| 0.188235   |
| 1831,1811   | 1831,1810| 1830,1810| 0.188235   |
| 1832,1811   | 1832,1810| 1831,1810| 0.188235   |
| 1833,1811   | 1832,1810| 1832,1810| 0.000000   |

In the end we go over this list mix the sources and store the result at
the destination.

```
tile_buffer[destination] = mix(tile_buffer[source_1],
                               tile_buffer[source_2],
                               mix_factor);
```

**Encoding**
When using a large textures or large seam margins this table can grow
large and reduce performance as data retrieval is slower, than the
operations it has to perform. To improve the performance we encode the
table so less data retrieval needs to be done.

* first `DeltaCopyPixelCommand` is delta encoded from
  `CopyPixelGroup#start_destination` and `start_source_1`. The others
  are delta encoded from the previous `DeltaCopyPixelCommand`.
* For performance reasons PixelCopyGroup#pixels are ordered from
  destination (left to right) for each row a new group would be created
  as the delta encoding most likely doesn't fit. When pixels cannot be
  delta encoded a new group will also be created.

**Compression rate**
When using Suzanne the compression rate is around 36% when using a seam
margin of 4 pixels. The compression rate may vary depending on seam
margin and model. For Suzanne the compression rate was around 36% for
various resolutions.

| Resolution | Margin | Decoded size | Encoded size | Compression |
| ---------- | ------ | ------------ | ------------ | ----------- |
| 2048x2048  | 4 px   | 353.052      | 128.101      | 36%         |
| 4096x4096  | 4 px   | 700.140      | 255.137      | 36%         |
| 8192x8192  | 4 px   | 1.419.320    | 513.802      | 36%         |
| 2048x2048  | 8 px   | 721.084      | 193.629      | 26%         |
| 4096x4096  | 8 px   | 1.444.968    | 388.110      | 26%         |

Pull Request: https://projects.blender.org/blender/blender/pulls/105336
This commit is contained in:
Jeroen Bakker
2023-03-09 16:11:01 +01:00
parent f27d6b9640
commit b0037c5c6b
8 changed files with 863 additions and 1 deletions

View File

@@ -9,8 +9,11 @@
#include "DNA_image_types.h"
#include "BLI_math.h"
#include "BLI_math_vector_types.hh"
#include "IMB_imbuf_types.h"
namespace blender::bke::image {
/** Type to use for UDIM tile numbers (1001). */
@@ -44,4 +47,44 @@ struct ImageTileWrapper {
return (tile_number - 1001) / 10;
}
};
template<typename T, int Channels = 4> struct ImageBufferAccessor {
static_assert(std::is_same_v<T, int> || std::is_same_v<T, float4>);
ImBuf &image_buffer;
ImageBufferAccessor(ImBuf &image_buffer) : image_buffer(image_buffer)
{
}
float4 read_pixel(const int2 coordinate)
{
if constexpr ((std::is_same_v<T, float4>)) {
int offset = (coordinate.y * image_buffer.x + coordinate.x) * Channels;
return float4(&image_buffer.rect_float[offset]);
}
if constexpr ((std::is_same_v<T, int>)) {
int offset = (coordinate.y * image_buffer.x + coordinate.x);
float4 result;
rgba_uchar_to_float(result,
static_cast<uchar *>(static_cast<void *>(&image_buffer.rect[offset])));
return result;
}
return float4();
}
void write_pixel(const int2 coordinate, float4 new_value)
{
if constexpr ((std::is_same_v<T, float>)) {
int offset = (coordinate.y * image_buffer.x + coordinate.x) * Channels;
copy_v4_v4(&image_buffer.rect_float[offset], new_value);
}
if constexpr ((std::is_same_v<T, int>)) {
int offset = (coordinate.y * image_buffer.x + coordinate.x);
rgba_float_to_uchar(static_cast<uchar *>(static_cast<void *>(&image_buffer.rect[offset])),
new_value);
}
}
};
} // namespace blender::bke::image

View File

@@ -3,8 +3,10 @@
#pragma once
#include <functional>
#include "BLI_math.h"
#include "BLI_math_vector_types.hh"
#include "BLI_math_vector.hh"
#include "BLI_rect.h"
#include "BLI_vector.hh"
@@ -233,6 +235,15 @@ struct NodeData {
}
}
void collect_dirty_tiles(Vector<image::TileNumber> &r_dirty_tiles)
{
for (UDIMTilePixels &tile : tiles) {
if (tile.flags.dirty) {
r_dirty_tiles.append_non_duplicates(tile.tile_number);
}
}
}
void clear_data()
{
tiles.clear();
@@ -245,11 +256,166 @@ struct NodeData {
MEM_delete(node_data);
}
};
/* -------------------------------------------------------------------- */
/** \name Fix non-manifold edge bleeding.
* \{ */
struct DeltaCopyPixelCommand {
char2 delta_source_1;
char2 delta_source_2;
uint8_t mix_factor;
DeltaCopyPixelCommand(char2 delta_source_1, char2 delta_source_2, uint8_t mix_factor)
: delta_source_1(delta_source_1), delta_source_2(delta_source_2), mix_factor(mix_factor)
{
}
};
struct CopyPixelGroup {
int2 start_destination;
int2 start_source_1;
int64_t start_delta_index;
int num_deltas;
};
/** Pixel copy command to mix 2 source pixels and write to a destination pixel. */
struct CopyPixelCommand {
/** Pixel coordinate to write to. */
int2 destination;
/** Pixel coordinate to read first source from. */
int2 source_1;
/** Pixel coordinate to read second source from. */
int2 source_2;
/** Factor to mix between first and second source. */
float mix_factor;
CopyPixelCommand() = default;
CopyPixelCommand(const CopyPixelGroup &group)
: destination(group.start_destination),
source_1(group.start_source_1),
source_2(),
mix_factor(0.0f)
{
}
template<typename T>
void mix_source_and_write_destination(image::ImageBufferAccessor<T> &tile_buffer) const
{
float4 source_color_1 = tile_buffer.read_pixel(source_1);
float4 source_color_2 = tile_buffer.read_pixel(source_2);
float4 destination_color = source_color_1 * (1.0f - mix_factor) + source_color_2 * mix_factor;
tile_buffer.write_pixel(destination, destination_color);
}
void apply(const DeltaCopyPixelCommand &item)
{
destination.x += 1;
source_1 += int2(item.delta_source_1);
source_2 = source_1 + int2(item.delta_source_2);
mix_factor = float(item.mix_factor) / 255.0f;
}
DeltaCopyPixelCommand encode_delta(const CopyPixelCommand &next_command) const
{
return DeltaCopyPixelCommand(char2(next_command.source_1 - source_1),
char2(next_command.source_2 - next_command.source_1),
uint8_t(next_command.mix_factor * 255));
}
bool can_be_extended(const CopyPixelCommand &command) const
{
/* Can only extend sequential pixels. */
if (destination.x != command.destination.x - 1 || destination.y != command.destination.y) {
return false;
}
/* Can only extend when the delta between with the previous source fits in a single byte.*/
int2 delta_source_1 = source_1 - command.source_1;
if (max_ii(UNPACK2(blender::math::abs(delta_source_1))) > 127) {
return false;
}
return true;
}
};
struct CopyPixelTile {
image::TileNumber tile_number;
Vector<CopyPixelGroup> groups;
Vector<DeltaCopyPixelCommand> command_deltas;
CopyPixelTile(image::TileNumber tile_number) : tile_number(tile_number)
{
}
void copy_pixels(ImBuf &tile_buffer, IndexRange group_range) const
{
if (tile_buffer.rect_float) {
image::ImageBufferAccessor<float4> accessor(tile_buffer);
copy_pixels<float4>(accessor, group_range);
}
else {
image::ImageBufferAccessor<int> accessor(tile_buffer);
copy_pixels<int>(accessor, group_range);
}
}
void print_compression_rate()
{
int decoded_size = command_deltas.size() * sizeof(CopyPixelCommand);
int encoded_size = groups.size() * sizeof(CopyPixelGroup) +
command_deltas.size() * sizeof(DeltaCopyPixelCommand);
printf("Tile %d compression rate: %d->%d = %d%%\n",
tile_number,
decoded_size,
encoded_size,
int(100.0 * float(encoded_size) / float(decoded_size)));
}
private:
template<typename T>
void copy_pixels(image::ImageBufferAccessor<T> &image_buffer, IndexRange group_range) const
{
for (const int64_t group_index : group_range) {
const CopyPixelGroup &group = groups[group_index];
CopyPixelCommand copy_command(group);
for (const DeltaCopyPixelCommand &item : Span<const DeltaCopyPixelCommand>(
&command_deltas[group.start_delta_index], group.num_deltas)) {
copy_command.apply(item);
copy_command.mix_source_and_write_destination<T>(image_buffer);
}
}
}
};
struct CopyPixelTiles {
Vector<CopyPixelTile> tiles;
std::optional<std::reference_wrapper<CopyPixelTile>> find_tile(image::TileNumber tile_number)
{
for (CopyPixelTile &tile : tiles) {
if (tile.tile_number == tile_number) {
return tile;
}
}
return std::nullopt;
}
void clear()
{
tiles.clear();
}
};
/** \} */
struct PBVHData {
/* Per UVPRimitive contains the paint data. */
PaintGeometryPrimitives geom_primitives;
/** Per ImageTile the pixels to copy to fix non-manifold bleeding. */
CopyPixelTiles tiles_copy_pixels;
void clear_data()
{
geom_primitives.clear();
@@ -259,5 +425,11 @@ struct PBVHData {
NodeData &BKE_pbvh_pixels_node_data_get(PBVHNode &node);
void BKE_pbvh_pixels_mark_image_dirty(PBVHNode &node, Image &image, ImageUser &image_user);
PBVHData &BKE_pbvh_pixels_data_get(PBVH &pbvh);
void BKE_pbvh_pixels_collect_dirty_tiles(PBVHNode &node, Vector<image::TileNumber> &r_dirty_tiles);
void BKE_pbvh_pixels_copy_pixels(PBVH &pbvh,
Image &image,
ImageUser &image_user,
image::TileNumber tile_number);
} // namespace blender::bke::pbvh::pixels

View File

@@ -251,6 +251,7 @@ set(SRC
intern/pbvh_bmesh.cc
intern/pbvh_colors.cc
intern/pbvh_pixels.cc
intern/pbvh_pixels_copy.cc
intern/pbvh_uv_islands.cc
intern/pointcache.c
intern/pointcloud.cc

View File

@@ -23,6 +23,7 @@
#include "bmesh.h"
#include "pbvh_intern.hh"
#include "pbvh_pixels_copy.hh"
#include "pbvh_uv_islands.hh"
namespace blender::bke::pbvh::pixels {
@@ -714,6 +715,9 @@ static bool update_pixels(PBVH *pbvh, Mesh *mesh, Image *image, ImageUser *image
apply_watertight_check(pbvh, image, image_user);
}
/* Add solution for non-manifold parts of the model. */
BKE_pbvh_pixels_copy_update(*pbvh, *image, *image_user, mesh_data);
/* Rebuild the undo regions. */
for (PBVHNode *node : nodes_to_update) {
NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
@@ -797,6 +801,13 @@ void BKE_pbvh_pixels_mark_image_dirty(PBVHNode &node, Image &image, ImageUser &i
node_data->flags.dirty = false;
}
}
void BKE_pbvh_pixels_collect_dirty_tiles(PBVHNode &node, Vector<image::TileNumber> &r_dirty_tiles)
{
NodeData *node_data = static_cast<NodeData *>(node.pixels.node_data);
node_data->collect_dirty_tiles(r_dirty_tiles);
}
} // namespace blender::bke::pbvh::pixels
using namespace blender::bke::pbvh::pixels;

View File

@@ -0,0 +1,576 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 Blender Foundation. All rights reserved. */
#include "BLI_array.hh"
#include "BLI_bit_vector.hh"
#include "BLI_math.h"
#include "BLI_math_vector.hh"
#include "BLI_task.hh"
#include "BLI_vector.hh"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
#include "BKE_image_wrappers.hh"
#include "BKE_pbvh.h"
#include "BKE_pbvh_pixels.hh"
#include "pbvh_intern.hh"
#include "pbvh_pixels_copy.hh"
#include "pbvh_uv_islands.hh"
namespace blender::bke::pbvh::pixels {
const int THREADING_GRAIN_SIZE = 128;
/** Coordinate space of a coordinate. */
enum class CoordSpace {
/**
* Coordinate is in UV coordinate space. As in unmodified from mesh data.
*/
UV,
/**
* Coordinate is in Tile coordinate space.
*
* With tile coordinate space each unit is a single pixel of the tile.
* Range is [0..buffer width].
*/
Tile,
};
template<CoordSpace Space> struct Vertex {
float2 coordinate;
};
template<CoordSpace Space> struct Edge {
Vertex<Space> vertex_1;
Vertex<Space> vertex_2;
};
/** Calculate the bounds of the given edge. */
static rcti get_bounds(const Edge<CoordSpace::Tile> &tile_edge)
{
rcti bounds;
BLI_rcti_init_minmax(&bounds);
BLI_rcti_do_minmax_v(&bounds, int2(tile_edge.vertex_1.coordinate));
BLI_rcti_do_minmax_v(&bounds, int2(tile_edge.vertex_2.coordinate));
return bounds;
}
/** Add a margin to the given bounds. */
static void add_margin(rcti &bounds, int margin)
{
bounds.xmin -= margin;
bounds.xmax += margin;
bounds.ymin -= margin;
bounds.ymax += margin;
}
/** Clamp bounds to be between 0,0 and the given resolution. */
static void clamp(rcti &bounds, int2 resolution)
{
rcti clamping_bounds;
BLI_rcti_init(&clamping_bounds, 0, resolution.x - 1, 0, resolution.y - 1);
BLI_rcti_isect(&bounds, &clamping_bounds, &bounds);
}
static const Vertex<CoordSpace::Tile> convert_coord_space(const Vertex<CoordSpace::UV> &uv_vertex,
const image::ImageTileWrapper image_tile,
const int2 tile_resolution)
{
return Vertex<CoordSpace::Tile>{(uv_vertex.coordinate - float2(image_tile.get_tile_offset())) *
float2(tile_resolution)};
}
static const Edge<CoordSpace::Tile> convert_coord_space(const Edge<CoordSpace::UV> &uv_edge,
const image::ImageTileWrapper image_tile,
const int2 tile_resolution)
{
return Edge<CoordSpace::Tile>{
convert_coord_space(uv_edge.vertex_1, image_tile, tile_resolution),
convert_coord_space(uv_edge.vertex_2, image_tile, tile_resolution),
};
}
class NonManifoldTileEdges : public Vector<Edge<CoordSpace::Tile>> {};
class NonManifoldUVEdges : public Vector<Edge<CoordSpace::UV>> {
public:
NonManifoldUVEdges(const uv_islands::MeshData &mesh_data)
{
int num_non_manifold_edges = count_non_manifold_edges(mesh_data);
reserve(num_non_manifold_edges);
for (const int primitive_id : mesh_data.looptris.index_range()) {
for (const int edge_id : mesh_data.primitive_to_edge_map[primitive_id]) {
if (is_manifold(mesh_data, edge_id)) {
continue;
}
const MLoopTri &loop_tri = mesh_data.looptris[primitive_id];
const uv_islands::MeshEdge &mesh_edge = mesh_data.edges[edge_id];
Edge<CoordSpace::UV> edge;
edge.vertex_1.coordinate = find_uv(mesh_data, loop_tri, mesh_edge.vert1);
edge.vertex_2.coordinate = find_uv(mesh_data, loop_tri, mesh_edge.vert2);
append(edge);
}
}
BLI_assert_msg(size() == num_non_manifold_edges,
"Incorrect number of non manifold edges added. ");
}
NonManifoldTileEdges extract_tile_edges(const image::ImageTileWrapper image_tile,
const int2 tile_resolution) const
{
NonManifoldTileEdges result;
for (const Edge<CoordSpace::UV> &uv_edge : *this) {
const Edge<CoordSpace::Tile> tile_edge = convert_coord_space(
uv_edge, image_tile, tile_resolution);
result.append(tile_edge);
}
return result;
}
private:
static int64_t count_non_manifold_edges(const uv_islands::MeshData &mesh_data)
{
int64_t result = 0;
for (const int primitive_id : mesh_data.looptris.index_range()) {
for (const int edge_id : mesh_data.primitive_to_edge_map[primitive_id]) {
if (is_manifold(mesh_data, edge_id)) {
continue;
}
result += 1;
}
}
return result;
}
static bool is_manifold(const uv_islands::MeshData &mesh_data, const int edge_id)
{
return mesh_data.edge_to_primitive_map[edge_id].size() == 2;
}
static float2 find_uv(const uv_islands::MeshData &mesh_data,
const MLoopTri &loop_tri,
int vertex_i)
{
for (int i = 0; i < 3; i++) {
int loop_i = loop_tri.tri[i];
const MLoop &loop = mesh_data.loops[loop_i];
if (loop.v == vertex_i) {
return mesh_data.uv_map[loop_i];
}
}
BLI_assert_unreachable();
return float2(0.0f);
}
};
class PixelNodesTileData : public Vector<std::reference_wrapper<UDIMTilePixels>> {
public:
PixelNodesTileData(PBVH &pbvh, const image::ImageTileWrapper &image_tile)
{
reserve(count_nodes(pbvh, image_tile));
for (PBVHNode &node : MutableSpan(pbvh.nodes, pbvh.totnode)) {
if (should_add_node(node, image_tile)) {
NodeData &node_data = *static_cast<NodeData *>(node.pixels.node_data);
UDIMTilePixels &tile_pixels = *node_data.find_tile_data(image_tile);
append(tile_pixels);
}
}
}
private:
static bool should_add_node(PBVHNode &node, const image::ImageTileWrapper &image_tile)
{
if ((node.flag & PBVH_Leaf) == 0) {
return false;
}
if (node.pixels.node_data == nullptr) {
return false;
}
NodeData &node_data = *static_cast<NodeData *>(node.pixels.node_data);
if (node_data.find_tile_data(image_tile) == nullptr) {
return false;
}
return true;
}
static int64_t count_nodes(PBVH &pbvh, const image::ImageTileWrapper &image_tile)
{
int64_t result = 0;
for (PBVHNode &node : MutableSpan(pbvh.nodes, pbvh.totnode)) {
if (should_add_node(node, image_tile)) {
result++;
}
}
return result;
}
};
/**
* Row contains intermediate data per pixel for a single image row. It is used during updating to
* encode pixels.
*/
struct Rows {
enum class PixelType {
Undecided,
/** This pixel is directly affected by a brush and doesn't need to be solved. */
Brush,
SelectedForCloserExamination,
/** This pixel will be copid from another pixel to solve non-manifold edge bleeding. */
CopyFromClosestEdge,
};
struct Pixel {
PixelType type;
float distance;
CopyPixelCommand copy_command;
/**
* Index of the edge in the list of non-manifold edges.
*
* The edge is kept to calculate athe mix factor between the two pixels that have chosen to
* be mixed.
*/
int64_t edge_index;
Pixel() = default;
void init(int2 coordinate)
{
copy_command.destination = coordinate;
copy_command.source_1 = coordinate;
copy_command.source_2 = coordinate;
copy_command.mix_factor = 0.0f;
type = PixelType::Undecided;
distance = std::numeric_limits<float>::max();
edge_index = -1;
}
};
int2 resolution;
int margin;
Array<Pixel> pixels;
struct RowView {
int row_number = 0;
/** Not owning pointer into Row.pixels starts at the start of the row.*/
MutableSpan<Pixel> pixels;
RowView() = delete;
RowView(Rows &rows, int64_t row_number)
: row_number(row_number),
pixels(
MutableSpan<Pixel>(&rows.pixels[row_number * rows.resolution.x], rows.resolution.x))
{
}
};
Rows(int2 resolution, int margin)
: resolution(resolution), margin(margin), pixels(resolution.x * resolution.y)
{
}
void init_pixels()
{
int64_t index = 0;
for (int y : IndexRange(resolution.y)) {
for (int64_t x : IndexRange(resolution.x)) {
int2 position(x, y);
pixels[index++].init(position);
}
}
}
/**
* Mark pixels that are painted on by the brush. Those pixels don't need to be updated, but will
* act as a source for other pixels.
*/
void mark_pixels_effected_by_brush(const PixelNodesTileData &nodes_tile_pixels)
{
for (const UDIMTilePixels &tile_pixels : nodes_tile_pixels) {
threading::parallel_for_each(
tile_pixels.pixel_rows, [&](const PackedPixelRow &encoded_pixels) {
for (int x = encoded_pixels.start_image_coordinate.x;
x < encoded_pixels.start_image_coordinate.x + encoded_pixels.num_pixels;
x++) {
int64_t index = encoded_pixels.start_image_coordinate.y * resolution.x + x;
pixels[index].type = PixelType::Brush;
pixels[index].distance = 0.0f;
}
});
}
}
/**
* Look for a second source pixel that will be blended with the first source pixel to improve
* the quality of the fix.
*
* - The second source pixel must be a neighbour pixel of the first source, or the same as the
* first source when no second pixel could be found.
* - The second source pixel must be a pixel that is painted on by the brush.
* - The second source pixel must be the second closest pixel , or the first source
* when no second pixel could be found.
*/
int2 find_second_source(int2 destination, int2 first_source)
{
rcti search_bounds;
BLI_rcti_init(&search_bounds,
max_ii(first_source.x - 1, 0),
min_ii(first_source.x + 1, resolution.x - 1),
max_ii(first_source.y - 1, 0),
min_ii(first_source.y + 1, resolution.y - 1));
/* Initialize to the first source, so when no other source could be found it will use the
* first_source. */
int2 found_source = first_source;
float found_distance = std::numeric_limits<float>().max();
for (int sy : IndexRange(search_bounds.ymin, BLI_rcti_size_y(&search_bounds) + 1)) {
for (int sx : IndexRange(search_bounds.xmin, BLI_rcti_size_x(&search_bounds) + 1)) {
int2 source(sx, sy);
/* Skip first source as it should be the closest and already selected. */
if (source == first_source) {
continue;
}
int pixel_index = sy * resolution.y + sx;
if (pixels[pixel_index].type != PixelType::Brush) {
continue;
}
float new_distance = blender::math::distance(float2(destination), float2(source));
if (new_distance < found_distance) {
found_distance = new_distance;
found_source = source;
}
}
}
return found_source;
}
float determine_mix_factor(const int2 destination,
const int2 source_1,
const int2 source_2,
const Edge<CoordSpace::Tile> &edge)
{
/* Use stable result when both sources are the same. */
if (source_1 == source_2) {
return 0.0f;
}
float2 clamped_to_edge;
float destination_lambda = closest_to_line_v2(
clamped_to_edge, float2(destination), edge.vertex_1.coordinate, edge.vertex_2.coordinate);
float source_1_lambda = closest_to_line_v2(
clamped_to_edge, float2(source_1), edge.vertex_1.coordinate, edge.vertex_2.coordinate);
float source_2_lambda = closest_to_line_v2(
clamped_to_edge, float2(source_2), edge.vertex_1.coordinate, edge.vertex_2.coordinate);
return clamp_f(
(destination_lambda - source_1_lambda) / (source_2_lambda - source_1_lambda), 0.0f, 1.0f);
}
void find_copy_source(Pixel &pixel, const NonManifoldTileEdges &tile_edges)
{
BLI_assert(pixel.type == PixelType::SelectedForCloserExamination);
rcti bounds;
BLI_rcti_init(&bounds,
pixel.copy_command.destination.x,
pixel.copy_command.destination.x,
pixel.copy_command.destination.y,
pixel.copy_command.destination.y);
add_margin(bounds, margin);
clamp(bounds, resolution);
float found_distance = std::numeric_limits<float>().max();
int2 found_source(0);
for (int sy : IndexRange(bounds.ymin, BLI_rcti_size_y(&bounds))) {
int pixel_index = sy * resolution.x;
for (int sx : IndexRange(bounds.xmin, BLI_rcti_size_x(&bounds))) {
Pixel &source = pixels[pixel_index + sx];
if (source.type != PixelType::Brush) {
continue;
}
float new_distance = blender::math::distance(float2(sx, sy),
float2(pixel.copy_command.destination));
if (new_distance < found_distance) {
found_source = int2(sx, sy);
found_distance = new_distance;
}
}
}
if (found_distance == std::numeric_limits<float>().max()) {
return;
}
pixel.type = PixelType::CopyFromClosestEdge;
pixel.distance = found_distance;
pixel.copy_command.source_1 = found_source;
pixel.copy_command.source_2 = find_second_source(pixel.copy_command.destination, found_source);
pixel.copy_command.mix_factor = determine_mix_factor(pixel.copy_command.destination,
pixel.copy_command.source_1,
pixel.copy_command.source_2,
tile_edges[pixel.edge_index]);
}
void find_copy_source(Vector<std::reference_wrapper<Pixel>> &selected_pixels,
const NonManifoldTileEdges &tile_edges)
{
threading::parallel_for(
IndexRange(selected_pixels.size()), THREADING_GRAIN_SIZE, [&](IndexRange range) {
for (int selected_pixel_index : range) {
Pixel &current_pixel = selected_pixels[selected_pixel_index];
find_copy_source(current_pixel, tile_edges);
}
});
}
Vector<std::reference_wrapper<Pixel>> filter_pixels_for_closer_examination(
const NonManifoldTileEdges &tile_edges)
{
Vector<std::reference_wrapper<Pixel>> selected_pixels;
selected_pixels.reserve(10000);
for (int tile_edge_index : tile_edges.index_range()) {
const Edge<CoordSpace::Tile> &tile_edge = tile_edges[tile_edge_index];
rcti edge_bounds = get_bounds(tile_edge);
add_margin(edge_bounds, margin);
clamp(edge_bounds, resolution);
for (const int64_t sy : IndexRange(edge_bounds.ymin, BLI_rcti_size_y(&edge_bounds))) {
for (const int64_t sx : IndexRange(edge_bounds.xmin, BLI_rcti_size_x(&edge_bounds))) {
const int64_t index = sy * resolution.x + sx;
Pixel &pixel = pixels[index];
if (pixel.type == PixelType::Brush) {
continue;
}
BLI_assert_msg(pixel.type != PixelType::CopyFromClosestEdge,
"PixelType::CopyFromClosestEdge isn't allowed to be set as it is set "
"when finding the pixels to copy.");
const float2 point(sx, sy);
float2 closest_edge_point;
closest_to_line_segment_v2(closest_edge_point,
point,
tile_edge.vertex_1.coordinate,
tile_edge.vertex_2.coordinate);
float distance_to_edge = blender::math::distance(closest_edge_point, point);
if (distance_to_edge < margin && distance_to_edge < pixel.distance) {
if (pixel.type != PixelType::SelectedForCloserExamination) {
selected_pixels.append(std::reference_wrapper<Pixel>(pixel));
}
pixel.type = PixelType::SelectedForCloserExamination;
pixel.distance = distance_to_edge;
pixel.edge_index = tile_edge_index;
}
}
}
}
return selected_pixels;
}
void pack_into(const Vector<std::reference_wrapper<Pixel>> &selected_pixels,
CopyPixelTile &copy_tile) const
{
std::optional<std::reference_wrapper<CopyPixelGroup>> last_group = std::nullopt;
std::optional<CopyPixelCommand> last_command = std::nullopt;
for (const Pixel &elem : selected_pixels) {
if (elem.type == PixelType::CopyFromClosestEdge) {
if (!last_command.has_value() || !last_command->can_be_extended(elem.copy_command)) {
CopyPixelGroup new_group = {elem.copy_command.destination - int2(1, 0),
elem.copy_command.source_1,
copy_tile.command_deltas.size(),
0};
copy_tile.groups.append(new_group);
last_group = copy_tile.groups.last();
last_command = CopyPixelCommand(*last_group);
}
DeltaCopyPixelCommand delta_command = last_command->encode_delta(elem.copy_command);
copy_tile.command_deltas.append(delta_command);
last_group->get().num_deltas++;
last_command = elem.copy_command;
}
}
}
}; // namespace blender::bke::pbvh::pixels
void BKE_pbvh_pixels_copy_update(PBVH &pbvh,
Image &image,
ImageUser &image_user,
const uv_islands::MeshData &mesh_data)
{
PBVHData &pbvh_data = BKE_pbvh_pixels_data_get(pbvh);
pbvh_data.tiles_copy_pixels.clear();
const NonManifoldUVEdges non_manifold_edges(mesh_data);
if (non_manifold_edges.is_empty()) {
/* Early exit: No non manifold edges detected. */
return;
}
ImageUser tile_user = image_user;
LISTBASE_FOREACH (ImageTile *, tile, &image.tiles) {
const image::ImageTileWrapper image_tile = image::ImageTileWrapper(tile);
tile_user.tile = image_tile.get_tile_number();
ImBuf *tile_buffer = BKE_image_acquire_ibuf(&image, &tile_user, nullptr);
if (tile_buffer == nullptr) {
continue;
}
const PixelNodesTileData nodes_tile_pixels(pbvh, image_tile);
int2 tile_resolution(tile_buffer->x, tile_buffer->y);
BKE_image_release_ibuf(&image, tile_buffer, nullptr);
NonManifoldTileEdges tile_edges = non_manifold_edges.extract_tile_edges(image_tile,
tile_resolution);
CopyPixelTile copy_tile(image_tile.get_tile_number());
Rows rows(tile_resolution, image.seam_margin);
rows.init_pixels();
rows.mark_pixels_effected_by_brush(nodes_tile_pixels);
Vector<std::reference_wrapper<Rows::Pixel>> selected_pixels =
rows.filter_pixels_for_closer_examination(tile_edges);
rows.find_copy_source(selected_pixels, tile_edges);
rows.pack_into(selected_pixels, copy_tile);
copy_tile.print_compression_rate();
pbvh_data.tiles_copy_pixels.tiles.append(copy_tile);
}
}
void BKE_pbvh_pixels_copy_pixels(PBVH &pbvh,
Image &image,
ImageUser &image_user,
image::TileNumber tile_number)
{
PBVHData &pbvh_data = BKE_pbvh_pixels_data_get(pbvh);
std::optional<std::reference_wrapper<CopyPixelTile>> pixel_tile =
pbvh_data.tiles_copy_pixels.find_tile(tile_number);
if (!pixel_tile.has_value()) {
/* No pixels need to be copied. */
return;
}
ImageUser tile_user = image_user;
tile_user.tile = tile_number;
ImBuf *tile_buffer = BKE_image_acquire_ibuf(&image, &tile_user, nullptr);
if (tile_buffer == nullptr) {
/* No tile buffer found to copy. */
return;
}
CopyPixelTile &tile = pixel_tile->get();
threading::parallel_for(tile.groups.index_range(), THREADING_GRAIN_SIZE, [&](IndexRange range) {
tile.copy_pixels(*tile_buffer, range);
});
BKE_image_release_ibuf(&image, tile_buffer, nullptr);
}
} // namespace blender::bke::pbvh::pixels

View File

@@ -0,0 +1,24 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2023 Blender Foundation. All rights reserved. */
#include "BLI_math.h"
#include "BLI_math_vector_types.hh"
#include "BLI_vector.hh"
#include "IMB_imbuf.h"
#include "IMB_imbuf_types.h"
#include "BKE_image_wrappers.hh"
#include "BKE_pbvh.h"
#include "BKE_pbvh_pixels.hh"
#include "pbvh_uv_islands.hh"
namespace blender::bke::pbvh::pixels {
void BKE_pbvh_pixels_copy_update(PBVH &pbvh,
Image &image,
ImageUser &image_user,
const uv_islands::MeshData &mesh_data);
} // namespace blender::bke::pbvh::pixels

View File

@@ -645,6 +645,7 @@ template<typename T> struct AssertUnitEpsilon {
} // namespace math
using char2 = blender::VecBase<int8_t, 2>;
using char3 = blender::VecBase<int8_t, 3>;
using uchar3 = blender::VecBase<uint8_t, 3>;

View File

@@ -511,6 +511,39 @@ static void do_mark_dirty_regions(void *__restrict userdata,
PBVHNode *node = data->nodes[n];
BKE_pbvh_pixels_mark_image_dirty(*node, *data->image_data.image, *data->image_data.image_user);
}
/* -------------------------------------------------------------------- */
/** \name Fix non-manifold edge bleeding.
* \{ */
static Vector<image::TileNumber> collect_dirty_tiles(Span<PBVHNode *> nodes)
{
Vector<image::TileNumber> dirty_tiles;
for (PBVHNode *node : nodes) {
BKE_pbvh_pixels_collect_dirty_tiles(*node, dirty_tiles);
}
return dirty_tiles;
}
static void fix_non_manifold_seam_bleeding(PBVH &pbvh,
TexturePaintingUserData &user_data,
Span<TileNumber> tile_numbers_to_fix)
{
for (image::TileNumber tile_number : tile_numbers_to_fix) {
BKE_pbvh_pixels_copy_pixels(
pbvh, *user_data.image_data.image, *user_data.image_data.image_user, tile_number);
}
}
static void fix_non_manifold_seam_bleeding(Object &ob,
const int totnode,
TexturePaintingUserData &user_data)
{
Vector<image::TileNumber> dirty_tiles = collect_dirty_tiles(
Span<PBVHNode *>(user_data.nodes, totnode));
fix_non_manifold_seam_bleeding(*ob.sculpt->pbvh, user_data, dirty_tiles);
}
/** \} */
} // namespace blender::ed::sculpt_paint::paint::image
@@ -568,6 +601,7 @@ void SCULPT_do_paint_brush_image(PaintModeSettings *paint_mode_settings,
BKE_pbvh_parallel_range_settings(&settings, true, texnodes_num);
BLI_task_parallel_range(0, texnodes_num, &data, do_push_undo_tile, &settings);
BLI_task_parallel_range(0, texnodes_num, &data, do_paint_pixels, &settings);
fix_non_manifold_seam_bleeding(*ob, texnodes_num, data);
TaskParallelSettings settings_flush;