diff --git a/source/blender/blenkernel/BKE_image_wrappers.hh b/source/blender/blenkernel/BKE_image_wrappers.hh index 7fa26066e73..fe01d9e1e62 100644 --- a/source/blender/blenkernel/BKE_image_wrappers.hh +++ b/source/blender/blenkernel/BKE_image_wrappers.hh @@ -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 struct ImageBufferAccessor { + static_assert(std::is_same_v || std::is_same_v); + + ImBuf &image_buffer; + + ImageBufferAccessor(ImBuf &image_buffer) : image_buffer(image_buffer) + { + } + + float4 read_pixel(const int2 coordinate) + { + if constexpr ((std::is_same_v)) { + int offset = (coordinate.y * image_buffer.x + coordinate.x) * Channels; + return float4(&image_buffer.rect_float[offset]); + } + if constexpr ((std::is_same_v)) { + int offset = (coordinate.y * image_buffer.x + coordinate.x); + float4 result; + rgba_uchar_to_float(result, + static_cast(static_cast(&image_buffer.rect[offset]))); + return result; + } + return float4(); + } + + void write_pixel(const int2 coordinate, float4 new_value) + { + if constexpr ((std::is_same_v)) { + 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)) { + int offset = (coordinate.y * image_buffer.x + coordinate.x); + rgba_float_to_uchar(static_cast(static_cast(&image_buffer.rect[offset])), + new_value); + } + } +}; + } // namespace blender::bke::image diff --git a/source/blender/blenkernel/BKE_pbvh_pixels.hh b/source/blender/blenkernel/BKE_pbvh_pixels.hh index 31dbad0abe1..f8f71c2a602 100644 --- a/source/blender/blenkernel/BKE_pbvh_pixels.hh +++ b/source/blender/blenkernel/BKE_pbvh_pixels.hh @@ -3,8 +3,10 @@ #pragma once +#include + #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 &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 + void mix_source_and_write_destination(image::ImageBufferAccessor &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 groups; + Vector 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 accessor(tile_buffer); + copy_pixels(accessor, group_range); + } + else { + image::ImageBufferAccessor accessor(tile_buffer); + copy_pixels(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 + void copy_pixels(image::ImageBufferAccessor &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( + &command_deltas[group.start_delta_index], group.num_deltas)) { + copy_command.apply(item); + copy_command.mix_source_and_write_destination(image_buffer); + } + } + } +}; + +struct CopyPixelTiles { + Vector tiles; + + std::optional> 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 &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 diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index 1e7a0ac316f..eeb9c5be9ed 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -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 diff --git a/source/blender/blenkernel/intern/pbvh_pixels.cc b/source/blender/blenkernel/intern/pbvh_pixels.cc index 4a8f70714fc..5149ca1c56f 100644 --- a/source/blender/blenkernel/intern/pbvh_pixels.cc +++ b/source/blender/blenkernel/intern/pbvh_pixels.cc @@ -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(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 &r_dirty_tiles) +{ + NodeData *node_data = static_cast(node.pixels.node_data); + node_data->collect_dirty_tiles(r_dirty_tiles); +} + } // namespace blender::bke::pbvh::pixels using namespace blender::bke::pbvh::pixels; diff --git a/source/blender/blenkernel/intern/pbvh_pixels_copy.cc b/source/blender/blenkernel/intern/pbvh_pixels_copy.cc new file mode 100644 index 00000000000..93b9faf639c --- /dev/null +++ b/source/blender/blenkernel/intern/pbvh_pixels_copy.cc @@ -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 struct Vertex { + float2 coordinate; +}; + +template struct Edge { + Vertex vertex_1; + Vertex vertex_2; +}; + +/** Calculate the bounds of the given edge. */ +static rcti get_bounds(const Edge &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 convert_coord_space(const Vertex &uv_vertex, + const image::ImageTileWrapper image_tile, + const int2 tile_resolution) +{ + return Vertex{(uv_vertex.coordinate - float2(image_tile.get_tile_offset())) * + float2(tile_resolution)}; +} + +static const Edge convert_coord_space(const Edge &uv_edge, + const image::ImageTileWrapper image_tile, + const int2 tile_resolution) +{ + return Edge{ + 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> {}; + +class NonManifoldUVEdges : public Vector> { + 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 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 &uv_edge : *this) { + const Edge 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> { + 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(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(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::max(); + edge_index = -1; + } + }; + + int2 resolution; + int margin; + Array pixels; + + struct RowView { + int row_number = 0; + /** Not owning pointer into Row.pixels starts at the start of the row.*/ + MutableSpan pixels; + RowView() = delete; + RowView(Rows &rows, int64_t row_number) + : row_number(row_number), + pixels( + MutableSpan(&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().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 &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().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().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> &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 ¤t_pixel = selected_pixels[selected_pixel_index]; + find_copy_source(current_pixel, tile_edges); + } + }); + } + + Vector> filter_pixels_for_closer_examination( + const NonManifoldTileEdges &tile_edges) + { + Vector> selected_pixels; + selected_pixels.reserve(10000); + + for (int tile_edge_index : tile_edges.index_range()) { + const Edge &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.type = PixelType::SelectedForCloserExamination; + pixel.distance = distance_to_edge; + pixel.edge_index = tile_edge_index; + } + } + } + } + return selected_pixels; + } + + void pack_into(const Vector> &selected_pixels, + CopyPixelTile ©_tile) const + { + std::optional> last_group = std::nullopt; + std::optional 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> 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> 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 diff --git a/source/blender/blenkernel/intern/pbvh_pixels_copy.hh b/source/blender/blenkernel/intern/pbvh_pixels_copy.hh new file mode 100644 index 00000000000..57c1d887cef --- /dev/null +++ b/source/blender/blenkernel/intern/pbvh_pixels_copy.hh @@ -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 \ No newline at end of file diff --git a/source/blender/blenlib/BLI_math_vector_types.hh b/source/blender/blenlib/BLI_math_vector_types.hh index 09d65b4c311..bb7134370e8 100644 --- a/source/blender/blenlib/BLI_math_vector_types.hh +++ b/source/blender/blenlib/BLI_math_vector_types.hh @@ -645,6 +645,7 @@ template struct AssertUnitEpsilon { } // namespace math +using char2 = blender::VecBase; using char3 = blender::VecBase; using uchar3 = blender::VecBase; diff --git a/source/blender/editors/sculpt_paint/sculpt_paint_image.cc b/source/blender/editors/sculpt_paint/sculpt_paint_image.cc index 5d7c4687bc1..01d8100a8ab 100644 --- a/source/blender/editors/sculpt_paint/sculpt_paint_image.cc +++ b/source/blender/editors/sculpt_paint/sculpt_paint_image.cc @@ -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 collect_dirty_tiles(Span nodes) +{ + Vector 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 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 dirty_tiles = collect_dirty_tiles( + Span(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;