This repository has been archived on 2023-10-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
blender-archive/source/blender/blenkernel/intern/pbvh_pixels_copy.cc
Hans Goudey 16fbadde36 Mesh: Replace MLoop struct with generic attributes
Implements #102359.

Split the `MLoop` struct into two separate integer arrays called
`corner_verts` and `corner_edges`, referring to the vertex each corner
is attached to and the next edge around the face at each corner. These
arrays can be sliced to give access to the edges or vertices in a face.
Then they are often referred to as "poly_verts" or "poly_edges".

The main benefits are halving the necessary memory bandwidth when only
one array is used and simplifications from using regular integer indices
instead of a special-purpose struct.

The commit also starts a renaming from "loop" to "corner" in mesh code.

Like the other mesh struct of array refactors, forward compatibility is
kept by writing files with the older format. This will be done until 4.0
to ease the transition process.

Looking at a small portion of the patch should give a good impression
for the rest of the changes. I tried to make the changes as small as
possible so it's easy to tell the correctness from the diff. Though I
found Blender developers have been very inventive over the last decade
when finding different ways to loop over the corners in a face.

For performance, nearly every piece of code that deals with `Mesh` is
slightly impacted. Any algorithm that is memory bottle-necked should
see an improvement. For example, here is a comparison of interpolating
a vertex float attribute to face corners (Ryzen 3700x):

**Before** (Average: 3.7 ms, Min: 3.4 ms)
```
threading::parallel_for(loops.index_range(), 4096, [&](IndexRange range) {
  for (const int64_t i : range) {
    dst[i] = src[loops[i].v];
  }
});
```

**After** (Average: 2.9 ms, Min: 2.6 ms)
```
array_utils::gather(src, corner_verts, dst);
```

That's an improvement of 28% to the average timings, and it's also a
simplification, since an index-based routine can be used instead.
For more examples using the new arrays, see the design task.

Pull Request: blender/blender#104424
2023-03-20 15:55:13 +01:00

578 lines
20 KiB
C++

/* 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++) {
const int loop_i = loop_tri.tri[i];
const int vert = mesh_data.corner_verts[loop_i];
if (vert == 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 copied 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 the 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 neighbor 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