Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeroen Bakker <jbakker>2022-04-15 17:39:50 +0300
committerJeroen Bakker <jeroen@blender.org>2022-04-15 17:40:01 +0300
commite96a809a68ef40225d5eddfd790644538e171a8d (patch)
tree1c679db4cda3d0ea7d6189e63dbfce020f57abf6
parent25196f8a369c354bf870b6144e3f43aa0a1d38e0 (diff)
PBVH Pixel extractor.
This patch contains an initial pixel extractor for PBVH and an initial paint brush implementation. PBVH is an accelleration structure blender uses internally to speed up 3d painting operations. At this moment it is extensively used by sculpt, vertex painting and weight painting. For the 3d texturing brush we will be using the PBVH for texture painting. Currently PBVH is organized to work on geometry (vertices, polygons and triangles). For texture painting this should be extended it to use pixels. {F12995467} Screen recording has been done on a Mac Mini with a 6 core 3.3 GHZ Intel processor. # Scope This patch only contains an extending uv seams to fix uv seams. This is not actually we want, but was easy to add to make the brush usable. Pixels are places in the PBVH_Leaf nodes. We want to introduce a special node for pixels, but that will be done in a separate patch to keep the code review small. This reduces the painting performance when using low and medium poly assets. In workbench textures aren't forced to be shown. For now use Material/Rendered view. # Rasterization process The rasterization process will generate the pixel information for a leaf node. In the future those leaf nodes will be split up into multiple leaf nodes to increase the performance when there isn't enough geometry. For this patch this was left out of scope. In order to do so every polygon should be uniquely assigned to a leaf node. For each leaf node for each polygon If polygon not assigned assign polygon to node. Polygons are to complicated to be used directly we have to split the polygons into triangles. For each leaf node for each polygon extract triangles from polygon. The list of triangles can be stored inside the leaf node. The list of polygons aren't needed anymore. Each triangle has: poly_index. vert_indices delta barycentric coordinate between x steps. Each triangle is rasterized in rows. Sequential pixels (in uv space) are stored in a single structure. image position barycentric coordinate of the first pixel number of pixels triangle index inside the leaf node. During the performed experiments we used a fairly simple rasterization process by finding the UV bounds of an triangle and calculate the barycentric coordinates per pixel inside the bounds. Even for complex models and huge images this process is normally finished within 0.5 second. It could be that we want to change this algorithm to reduce hickups when nodes are initialized during a stroke. Reviewed By: brecht Maniphest Tasks: T96710 Differential Revision: https://developer.blender.org/D14504
-rw-r--r--source/blender/blenkernel/BKE_paint.h19
-rw-r--r--source/blender/blenkernel/BKE_pbvh.h21
-rw-r--r--source/blender/blenkernel/BKE_pbvh_pixels.hh184
-rw-r--r--source/blender/blenkernel/CMakeLists.txt1
-rw-r--r--source/blender/blenkernel/intern/material.c8
-rw-r--r--source/blender/blenkernel/intern/paint.c20
-rw-r--r--source/blender/blenkernel/intern/paint_canvas.cc51
-rw-r--r--source/blender/blenkernel/intern/pbvh.c14
-rw-r--r--source/blender/blenkernel/intern/pbvh_intern.h13
-rw-r--r--source/blender/blenkernel/intern/pbvh_pixels.cc393
-rw-r--r--source/blender/editors/include/ED_sculpt.h3
-rw-r--r--source/blender/editors/sculpt_paint/CMakeLists.txt1
-rw-r--r--source/blender/editors/sculpt_paint/sculpt.c127
-rw-r--r--source/blender/editors/sculpt_paint/sculpt_intern.h32
-rw-r--r--source/blender/editors/sculpt_paint/sculpt_paint_color.c8
-rw-r--r--source/blender/editors/sculpt_paint/sculpt_paint_image.cc432
-rw-r--r--source/blender/makesdna/DNA_material_types.h2
-rw-r--r--source/blender/makesdna/DNA_scene_types.h1
18 files changed, 1295 insertions, 35 deletions
diff --git a/source/blender/blenkernel/BKE_paint.h b/source/blender/blenkernel/BKE_paint.h
index db773d34cdc..4ae37095411 100644
--- a/source/blender/blenkernel/BKE_paint.h
+++ b/source/blender/blenkernel/BKE_paint.h
@@ -30,7 +30,9 @@ struct EdgeSet;
struct EnumPropertyItem;
struct GHash;
struct GridPaintMask;
+struct Image;
struct ImagePool;
+struct ImageUser;
struct ListBase;
struct MLoop;
struct MLoopTri;
@@ -650,6 +652,11 @@ typedef struct SculptSession {
*/
bool sticky_shading_color;
+ /**
+ * Last used painting canvas key.
+ */
+ char *last_paint_canvas_key;
+
} SculptSession;
void BKE_sculptsession_free(struct Object *ob);
@@ -727,8 +734,16 @@ enum {
};
/* paint_canvas.cc */
-struct Image *BKE_paint_canvas_image_get(const struct PaintModeSettings *settings,
- struct Object *ob);
+/**
+ * Create a key that can be used to compare with previous ones to identify changes.
+ * The resulting 'string' is owned by the caller.
+ */
+char *BKE_paint_canvas_key_get(struct PaintModeSettings *settings, struct Object *ob);
+
+bool BKE_paint_canvas_image_get(struct PaintModeSettings *settings,
+ struct Object *ob,
+ struct Image **r_image,
+ struct ImageUser **r_image_user);
int BKE_paint_canvas_uvmap_layer_index_get(const struct PaintModeSettings *settings,
struct Object *ob);
diff --git a/source/blender/blenkernel/BKE_pbvh.h b/source/blender/blenkernel/BKE_pbvh.h
index 775847f27f8..bb918fcfdcb 100644
--- a/source/blender/blenkernel/BKE_pbvh.h
+++ b/source/blender/blenkernel/BKE_pbvh.h
@@ -31,10 +31,13 @@ struct MLoopTri;
struct MPoly;
struct MVert;
struct Mesh;
+struct MeshElemMap;
struct PBVH;
struct PBVHNode;
struct SubdivCCG;
struct TaskParallelSettings;
+struct Image;
+struct ImageUser;
struct MeshElemMap;
typedef struct PBVH PBVH;
@@ -48,6 +51,15 @@ typedef struct {
float (*color)[4];
} PBVHColorBufferNode;
+typedef struct PBVHPixelsNode {
+ /**
+ * Contains triangle/pixel data used during texture painting.
+ *
+ * Contains #blender::bke::pbvh::pixels::NodeData.
+ */
+ void *node_data;
+} PBVHPixelsNode;
+
typedef enum {
PBVH_Leaf = 1 << 0,
@@ -66,6 +78,8 @@ typedef enum {
PBVH_UpdateTopology = 1 << 13,
PBVH_UpdateColor = 1 << 14,
+ PBVH_RebuildPixels = 1 << 15,
+
} PBVHNodeFlags;
typedef struct PBVHFrustumPlanes {
@@ -127,6 +141,12 @@ void BKE_pbvh_build_bmesh(PBVH *pbvh,
struct BMLog *log,
int cd_vert_node_offset,
int cd_face_node_offset);
+
+void BKE_pbvh_build_pixels(PBVH *pbvh,
+ const struct MLoop *mloop,
+ struct CustomData *ldata,
+ struct Image *image,
+ struct ImageUser *image_user);
void BKE_pbvh_free(PBVH *pbvh);
/* Hierarchical Search in the BVH, two methods:
@@ -287,6 +307,7 @@ bool BKE_pbvh_node_fully_masked_get(PBVHNode *node);
void BKE_pbvh_node_fully_unmasked_set(PBVHNode *node, int fully_masked);
bool BKE_pbvh_node_fully_unmasked_get(PBVHNode *node);
+void BKE_pbvh_mark_rebuild_pixels(PBVH *pbvh);
void BKE_pbvh_vert_mark_update(PBVH *pbvh, int index);
void BKE_pbvh_node_get_grids(PBVH *pbvh,
diff --git a/source/blender/blenkernel/BKE_pbvh_pixels.hh b/source/blender/blenkernel/BKE_pbvh_pixels.hh
new file mode 100644
index 00000000000..35eb340d0a1
--- /dev/null
+++ b/source/blender/blenkernel/BKE_pbvh_pixels.hh
@@ -0,0 +1,184 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright 2022 Blender Foundation. All rights reserved. */
+
+#pragma once
+
+#include "BLI_math.h"
+#include "BLI_math_vec_types.hh"
+#include "BLI_rect.h"
+#include "BLI_vector.hh"
+
+#include "DNA_image_types.h"
+#include "DNA_meshdata_types.h"
+
+#include "BKE_image.h"
+#include "BKE_image_wrappers.hh"
+
+#include "IMB_imbuf_types.h"
+
+namespace blender::bke::pbvh::pixels {
+
+struct TrianglePaintInput {
+ int3 vert_indices;
+ /**
+ * Delta barycentric coordinates between 2 neighbouring UV's in the U direction.
+ *
+ * Only the first two coordinates are stored. The third should be recalculated
+ */
+ float2 delta_barycentric_coord_u;
+
+ /**
+ * Initially only the vert indices are known.
+ *
+ * delta_barycentric_coord_u is initialized in a later stage as it requires image tile
+ * dimensions.
+ */
+ TrianglePaintInput(const int3 vert_indices)
+ : vert_indices(vert_indices), delta_barycentric_coord_u(0.0f, 0.0f)
+ {
+ }
+};
+
+/**
+ * Data shared between pixels that belong to the same triangle.
+ *
+ * Data is stored as a list of structs, grouped by usage to improve performance (improves CPU
+ * cache prefetching).
+ */
+struct Triangles {
+ /** Data accessed by the inner loop of the painting brush. */
+ Vector<TrianglePaintInput> paint_input;
+
+ public:
+ void append(const int3 vert_indices)
+ {
+ this->paint_input.append(TrianglePaintInput(vert_indices));
+ }
+
+ TrianglePaintInput &get_paint_input(const int index)
+ {
+ return paint_input[index];
+ }
+
+ const TrianglePaintInput &get_paint_input(const int index) const
+ {
+ return paint_input[index];
+ }
+
+ void clear()
+ {
+ paint_input.clear();
+ }
+
+ uint64_t size() const
+ {
+ return paint_input.size();
+ }
+
+ uint64_t mem_size() const
+ {
+ return paint_input.size() * sizeof(TrianglePaintInput);
+ }
+};
+
+/**
+ * Encode sequential pixels to reduce memory footprint.
+ */
+struct PackedPixelRow {
+ /** Barycentric coordinate of the first pixel. */
+ float2 start_barycentric_coord;
+ /** Image coordinate starting of the first pixel. */
+ ushort2 start_image_coordinate;
+ /** Number of sequential pixels encoded in this package. */
+ ushort num_pixels;
+ /** Reference to the pbvh triangle index. */
+ ushort triangle_index;
+};
+
+/**
+ * Node pixel data containing the pixels for a single UDIM tile.
+ */
+struct UDIMTilePixels {
+ /** UDIM Tile number. */
+ short tile_number;
+
+ struct {
+ bool dirty : 1;
+ } flags;
+
+ /* Dirty region of the tile in image space. */
+ rcti dirty_region;
+
+ Vector<PackedPixelRow> pixel_rows;
+
+ UDIMTilePixels()
+ {
+ flags.dirty = false;
+ BLI_rcti_init_minmax(&dirty_region);
+ }
+
+ void mark_dirty(const PackedPixelRow &pixel_row)
+ {
+ int2 start_image_coord(pixel_row.start_image_coordinate.x, pixel_row.start_image_coordinate.y);
+ BLI_rcti_do_minmax_v(&dirty_region, start_image_coord);
+ BLI_rcti_do_minmax_v(&dirty_region, start_image_coord + int2(pixel_row.num_pixels + 1, 0));
+ flags.dirty = true;
+ }
+
+ void clear_dirty()
+ {
+ BLI_rcti_init_minmax(&dirty_region);
+ flags.dirty = false;
+ }
+};
+
+struct NodeData {
+ struct {
+ bool dirty : 1;
+ } flags;
+
+ Vector<UDIMTilePixels> tiles;
+ Triangles triangles;
+
+ NodeData()
+ {
+ flags.dirty = false;
+ }
+
+ UDIMTilePixels *find_tile_data(const image::ImageTileWrapper &image_tile)
+ {
+ for (UDIMTilePixels &tile : tiles) {
+ if (tile.tile_number == image_tile.get_tile_number()) {
+ return &tile;
+ }
+ }
+ return nullptr;
+ }
+
+ void mark_region(Image &image, const image::ImageTileWrapper &image_tile, ImBuf &image_buffer)
+ {
+ UDIMTilePixels *tile = find_tile_data(image_tile);
+ if (tile && tile->flags.dirty) {
+ BKE_image_partial_update_mark_region(
+ &image, image_tile.image_tile, &image_buffer, &tile->dirty_region);
+ tile->clear_dirty();
+ }
+ }
+
+ void clear_data()
+ {
+ tiles.clear();
+ triangles.clear();
+ }
+
+ static void free_func(void *instance)
+ {
+ NodeData *node_data = static_cast<NodeData *>(instance);
+ MEM_delete(node_data);
+ }
+};
+
+NodeData &BKE_pbvh_pixels_node_data_get(PBVHNode &node);
+void BKE_pbvh_pixels_mark_image_dirty(PBVHNode &node, Image &image, ImageUser &image_user);
+
+} // namespace blender::bke::pbvh::pixels
diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt
index ce4131a0627..710e2900d78 100644
--- a/source/blender/blenkernel/CMakeLists.txt
+++ b/source/blender/blenkernel/CMakeLists.txt
@@ -244,6 +244,7 @@ set(SRC
intern/pbvh.cc
intern/pbvh.c
intern/pbvh_bmesh.c
+ intern/pbvh_pixels.cc
intern/pointcache.c
intern/pointcloud.cc
intern/preferences.c
diff --git a/source/blender/blenkernel/intern/material.c b/source/blender/blenkernel/intern/material.c
index bc569956f66..4caaf314888 100644
--- a/source/blender/blenkernel/intern/material.c
+++ b/source/blender/blenkernel/intern/material.c
@@ -1423,13 +1423,15 @@ static bool fill_texpaint_slots_cb(bNode *node, void *userdata)
case SH_NODE_TEX_IMAGE: {
TexPaintSlot *slot = &ma->texpaintslot[index];
slot->ima = (Image *)node->id;
- slot->interp = ((NodeTexImage *)node->storage)->interpolation;
+ NodeTexImage *storage = (NodeTexImage *)node->storage;
+ slot->interp = storage->interpolation;
+ slot->image_user = &storage->iuser;
/* for new renderer, we need to traverse the treeback in search of a UV node */
bNode *uvnode = nodetree_uv_node_recursive(node);
if (uvnode) {
- NodeShaderUVMap *storage = (NodeShaderUVMap *)uvnode->storage;
- slot->uvname = storage->uv_map;
+ NodeShaderUVMap *uv_storage = (NodeShaderUVMap *)uvnode->storage;
+ slot->uvname = uv_storage->uv_map;
/* set a value to index so UI knows that we have a valid pointer for the mesh */
slot->valid = true;
}
diff --git a/source/blender/blenkernel/intern/paint.c b/source/blender/blenkernel/intern/paint.c
index eb3f47760fc..c0195e5abc8 100644
--- a/source/blender/blenkernel/intern/paint.c
+++ b/source/blender/blenkernel/intern/paint.c
@@ -1511,6 +1511,8 @@ void BKE_sculptsession_free(Object *ob)
BKE_sculptsession_free_vwpaint_data(ob->sculpt);
+ MEM_SAFE_FREE(ss->last_paint_canvas_key);
+
MEM_freeN(ss);
ob->sculpt = NULL;
@@ -1771,6 +1773,24 @@ static void sculpt_update_object(Depsgraph *depsgraph,
}
}
+ /*
+ * We should rebuild the PBVH_pixels when painting canvas changes.
+ *
+ * The relevant changes are stored/encoded in the paint canvas key.
+ * These include the active uv map, and resolutions.
+ */
+ if (U.experimental.use_sculpt_texture_paint && ss->pbvh) {
+ char *paint_canvas_key = BKE_paint_canvas_key_get(&scene->toolsettings->paint_mode, ob);
+ if (ss->last_paint_canvas_key == NULL || !STREQ(paint_canvas_key, ss->last_paint_canvas_key)) {
+ MEM_SAFE_FREE(ss->last_paint_canvas_key);
+ ss->last_paint_canvas_key = paint_canvas_key;
+ BKE_pbvh_mark_rebuild_pixels(ss->pbvh);
+ }
+ else {
+ MEM_freeN(paint_canvas_key);
+ }
+ }
+
/* We could be more precise when we have access to the active tool. */
const bool use_paint_slots = (ob->mode & OB_MODE_SCULPT) != 0;
if (use_paint_slots) {
diff --git a/source/blender/blenkernel/intern/paint_canvas.cc b/source/blender/blenkernel/intern/paint_canvas.cc
index c1145164642..b72418d88c0 100644
--- a/source/blender/blenkernel/intern/paint_canvas.cc
+++ b/source/blender/blenkernel/intern/paint_canvas.cc
@@ -6,9 +6,12 @@
#include "DNA_scene_types.h"
#include "BKE_customdata.h"
+#include "BKE_image.h"
#include "BKE_material.h"
#include "BKE_paint.h"
+#include "IMB_imbuf_types.h"
+
namespace blender::bke::paint::canvas {
static TexPaintSlot *get_active_slot(Object *ob)
{
@@ -33,22 +36,35 @@ extern "C" {
using namespace blender::bke::paint::canvas;
-Image *BKE_paint_canvas_image_get(const struct PaintModeSettings *settings, struct Object *ob)
+bool BKE_paint_canvas_image_get(PaintModeSettings *settings,
+ Object *ob,
+ Image **r_image,
+ ImageUser **r_image_user)
{
+ *r_image = nullptr;
+ *r_image_user = nullptr;
+
switch (settings->canvas_source) {
case PAINT_CANVAS_SOURCE_COLOR_ATTRIBUTE:
- return nullptr;
+ break;
+
case PAINT_CANVAS_SOURCE_IMAGE:
- return settings->canvas_image;
+ *r_image = settings->canvas_image;
+ *r_image_user = &settings->image_user;
+ break;
+
case PAINT_CANVAS_SOURCE_MATERIAL: {
TexPaintSlot *slot = get_active_slot(ob);
if (slot == nullptr) {
break;
}
- return slot->ima;
+
+ *r_image = slot->ima;
+ *r_image_user = slot->image_user;
+ break;
}
}
- return nullptr;
+ return *r_image != nullptr;
}
int BKE_paint_canvas_uvmap_layer_index_get(const struct PaintModeSettings *settings,
@@ -87,4 +103,29 @@ int BKE_paint_canvas_uvmap_layer_index_get(const struct PaintModeSettings *setti
}
return -1;
}
+
+char *BKE_paint_canvas_key_get(struct PaintModeSettings *settings, struct Object *ob)
+{
+ std::stringstream ss;
+ int active_uv_map_layer_index = BKE_paint_canvas_uvmap_layer_index_get(settings, ob);
+ ss << "UV_MAP:" << active_uv_map_layer_index;
+
+ Image *image;
+ ImageUser *image_user;
+ if (BKE_paint_canvas_image_get(settings, ob, &image, &image_user)) {
+ ImageUser tile_user = *image_user;
+ LISTBASE_FOREACH (ImageTile *, image_tile, &image->tiles) {
+ tile_user.tile = image_tile->tile_number;
+ ImBuf *image_buffer = BKE_image_acquire_ibuf(image, &tile_user, nullptr);
+ if (!image_buffer) {
+ continue;
+ }
+ ss << ",TILE_" << image_tile->tile_number;
+ ss << "(" << image_buffer->x << "," << image_buffer->y << ")";
+ BKE_image_release_ibuf(image, image_buffer, nullptr);
+ }
+ }
+
+ return BLI_strdup(ss.str().c_str());
+}
}
diff --git a/source/blender/blenkernel/intern/pbvh.c b/source/blender/blenkernel/intern/pbvh.c
index 5d307697208..e91f360ef22 100644
--- a/source/blender/blenkernel/intern/pbvh.c
+++ b/source/blender/blenkernel/intern/pbvh.c
@@ -686,6 +686,8 @@ void BKE_pbvh_free(PBVH *pbvh)
if (node->bm_other_verts) {
BLI_gset_free(node->bm_other_verts, NULL);
}
+
+ pbvh_pixels_free(node);
}
}
@@ -1769,7 +1771,7 @@ BMesh *BKE_pbvh_get_bmesh(PBVH *pbvh)
void BKE_pbvh_node_mark_update(PBVHNode *node)
{
node->flag |= PBVH_UpdateNormals | PBVH_UpdateBB | PBVH_UpdateOriginalBB |
- PBVH_UpdateDrawBuffers | PBVH_UpdateRedraw;
+ PBVH_UpdateDrawBuffers | PBVH_UpdateRedraw | PBVH_RebuildPixels;
}
void BKE_pbvh_node_mark_update_mask(PBVHNode *node)
@@ -1782,6 +1784,16 @@ void BKE_pbvh_node_mark_update_color(PBVHNode *node)
node->flag |= PBVH_UpdateColor | PBVH_UpdateDrawBuffers | PBVH_UpdateRedraw;
}
+void BKE_pbvh_mark_rebuild_pixels(PBVH *pbvh)
+{
+ for (int n = 0; n < pbvh->totnode; n++) {
+ PBVHNode *node = &pbvh->nodes[n];
+ if (node->flag & PBVH_Leaf) {
+ node->flag |= PBVH_RebuildPixels;
+ }
+ }
+}
+
void BKE_pbvh_node_mark_update_visibility(PBVHNode *node)
{
node->flag |= PBVH_UpdateVisibility | PBVH_RebuildDrawBuffers | PBVH_UpdateDrawBuffers |
diff --git a/source/blender/blenkernel/intern/pbvh_intern.h b/source/blender/blenkernel/intern/pbvh_intern.h
index ea1f0632f32..77bd00da50a 100644
--- a/source/blender/blenkernel/intern/pbvh_intern.h
+++ b/source/blender/blenkernel/intern/pbvh_intern.h
@@ -6,6 +6,10 @@
* \ingroup bke
*/
+#ifdef __cplusplus
+extern "C" {
+#endif
+
/* Axis-aligned bounding box */
typedef struct {
float bmin[3], bmax[3];
@@ -111,6 +115,7 @@ struct PBVHNode {
/* Used to store the brush color during a stroke and composite it over the original color */
PBVHColorBufferNode color_buffer;
+ PBVHPixelsNode pixels;
};
typedef enum {
@@ -260,3 +265,11 @@ bool pbvh_bmesh_node_nearest_to_ray(PBVHNode *node,
bool use_original);
void pbvh_bmesh_normals_update(PBVHNode **nodes, int totnode);
+
+/* pbvh_pixels.hh */
+void pbvh_pixels_free(PBVHNode *node);
+void pbvh_pixels_free_brush_test(PBVHNode *node);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/source/blender/blenkernel/intern/pbvh_pixels.cc b/source/blender/blenkernel/intern/pbvh_pixels.cc
new file mode 100644
index 00000000000..d8dd2f4b382
--- /dev/null
+++ b/source/blender/blenkernel/intern/pbvh_pixels.cc
@@ -0,0 +1,393 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright 2022 Blender Foundation. All rights reserved. */
+
+#include "BKE_customdata.h"
+#include "BKE_mesh_mapping.h"
+#include "BKE_pbvh.h"
+#include "BKE_pbvh_pixels.hh"
+
+#include "DNA_image_types.h"
+#include "DNA_mesh_types.h"
+#include "DNA_meshdata_types.h"
+#include "DNA_object_types.h"
+
+#include "BLI_math.h"
+#include "BLI_task.h"
+
+#include "BKE_image_wrappers.hh"
+
+#include "bmesh.h"
+
+#include "pbvh_intern.h"
+
+namespace blender::bke::pbvh::pixels {
+
+/** Durind debugging this check could be enabled. It will write to each image pixel that is covered
+ * by the pbvh. */
+constexpr bool USE_WATERTIGHT_CHECK = false;
+
+/**
+ * Calculate the delta of two neighbour uv coordinates in the given image buffer.
+ */
+static float2 calc_barycentric_delta(const float2 uvs[3],
+ const float2 start_uv,
+ const float2 end_uv)
+{
+
+ float3 start_barycentric;
+ barycentric_weights_v2(uvs[0], uvs[1], uvs[2], start_uv, start_barycentric);
+ float3 end_barycentric;
+ barycentric_weights_v2(uvs[0], uvs[1], uvs[2], end_uv, end_barycentric);
+ float3 barycentric = end_barycentric - start_barycentric;
+ return float2(barycentric.x, barycentric.y);
+}
+
+static float2 calc_barycentric_delta_x(const ImBuf *image_buffer,
+ const float2 uvs[3],
+ const int x,
+ const int y)
+{
+ const float2 start_uv(float(x) / image_buffer->x, float(y) / image_buffer->y);
+ const float2 end_uv(float(x + 1) / image_buffer->x, float(y) / image_buffer->y);
+ return calc_barycentric_delta(uvs, start_uv, end_uv);
+}
+
+static void extract_barycentric_pixels(UDIMTilePixels &tile_data,
+ const ImBuf *image_buffer,
+ const int triangle_index,
+ const float2 uvs[3],
+ const int minx,
+ const int miny,
+ const int maxx,
+ const int maxy)
+{
+ for (int y = miny; y < maxy; y++) {
+ bool start_detected = false;
+ PackedPixelRow pixel_row;
+ pixel_row.triangle_index = triangle_index;
+ pixel_row.num_pixels = 0;
+ int x;
+
+ for (x = minx; x < maxx; x++) {
+ float2 uv(float(x) / image_buffer->x, float(y) / image_buffer->y);
+ float3 barycentric_weights;
+ barycentric_weights_v2(uvs[0], uvs[1], uvs[2], uv, barycentric_weights);
+
+ const bool is_inside = barycentric_inside_triangle_v2(barycentric_weights);
+ if (!start_detected && is_inside) {
+ start_detected = true;
+ pixel_row.start_image_coordinate = ushort2(x, y);
+ pixel_row.start_barycentric_coord = float2(barycentric_weights.x, barycentric_weights.y);
+ }
+ else if (start_detected && !is_inside) {
+ break;
+ }
+ }
+
+ if (!start_detected) {
+ continue;
+ }
+ pixel_row.num_pixels = x - pixel_row.start_image_coordinate.x;
+ tile_data.pixel_rows.append(pixel_row);
+ }
+}
+
+static void init_triangles(PBVH *pbvh, PBVHNode *node, NodeData *node_data, const MLoop *mloop)
+{
+ for (int i = 0; i < node->totprim; i++) {
+ const MLoopTri *lt = &pbvh->looptri[node->prim_indices[i]];
+ node_data->triangles.append(
+ int3(mloop[lt->tri[0]].v, mloop[lt->tri[1]].v, mloop[lt->tri[2]].v));
+ }
+}
+
+struct EncodePixelsUserData {
+ Image *image;
+ ImageUser *image_user;
+ PBVH *pbvh;
+ Vector<PBVHNode *> *nodes;
+ MLoopUV *ldata_uv;
+};
+
+static void do_encode_pixels(void *__restrict userdata,
+ const int n,
+ const TaskParallelTLS *__restrict UNUSED(tls))
+{
+ EncodePixelsUserData *data = static_cast<EncodePixelsUserData *>(userdata);
+ Image *image = data->image;
+ ImageUser image_user = *data->image_user;
+ PBVH *pbvh = data->pbvh;
+ PBVHNode *node = (*data->nodes)[n];
+ NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
+ LISTBASE_FOREACH (ImageTile *, tile, &data->image->tiles) {
+ image::ImageTileWrapper image_tile(tile);
+ image_user.tile = image_tile.get_tile_number();
+ ImBuf *image_buffer = BKE_image_acquire_ibuf(image, &image_user, nullptr);
+ if (image_buffer == nullptr) {
+ continue;
+ }
+
+ float2 tile_offset = float2(image_tile.get_tile_offset());
+ UDIMTilePixels tile_data;
+
+ Triangles &triangles = node_data->triangles;
+ for (int triangle_index = 0; triangle_index < triangles.size(); triangle_index++) {
+ const MLoopTri *lt = &pbvh->looptri[node->prim_indices[triangle_index]];
+ float2 uvs[3] = {
+ float2(data->ldata_uv[lt->tri[0]].uv) - tile_offset,
+ float2(data->ldata_uv[lt->tri[1]].uv) - tile_offset,
+ float2(data->ldata_uv[lt->tri[2]].uv) - tile_offset,
+ };
+
+ const float minv = clamp_f(min_fff(uvs[0].y, uvs[1].y, uvs[2].y), 0.0f, 1.0f);
+ const int miny = floor(minv * image_buffer->y);
+ const float maxv = clamp_f(max_fff(uvs[0].y, uvs[1].y, uvs[2].y), 0.0f, 1.0f);
+ const int maxy = min_ii(ceil(maxv * image_buffer->y), image_buffer->y);
+ const float minu = clamp_f(min_fff(uvs[0].x, uvs[1].x, uvs[2].x), 0.0f, 1.0f);
+ const int minx = floor(minu * image_buffer->x);
+ const float maxu = clamp_f(max_fff(uvs[0].x, uvs[1].x, uvs[2].x), 0.0f, 1.0f);
+ const int maxx = min_ii(ceil(maxu * image_buffer->x), image_buffer->x);
+
+ TrianglePaintInput &triangle = triangles.get_paint_input(triangle_index);
+ triangle.delta_barycentric_coord_u = calc_barycentric_delta_x(image_buffer, uvs, minx, miny);
+ extract_barycentric_pixels(
+ tile_data, image_buffer, triangle_index, uvs, minx, miny, maxx, maxy);
+ }
+
+ BKE_image_release_ibuf(image, image_buffer, nullptr);
+
+ if (tile_data.pixel_rows.is_empty()) {
+ continue;
+ }
+
+ tile_data.tile_number = image_tile.get_tile_number();
+ node_data->tiles.append(tile_data);
+ }
+}
+
+static bool should_pixels_be_updated(PBVHNode *node)
+{
+ if ((node->flag & PBVH_Leaf) == 0) {
+ return false;
+ }
+ if ((node->flag & PBVH_RebuildPixels) != 0) {
+ return true;
+ }
+ NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
+ if (node_data != nullptr) {
+ return false;
+ }
+ return true;
+}
+
+static int64_t count_nodes_to_update(PBVH *pbvh)
+{
+ int64_t result = 0;
+ for (int n = 0; n < pbvh->totnode; n++) {
+ PBVHNode *node = &pbvh->nodes[n];
+ if (should_pixels_be_updated(node)) {
+ result++;
+ }
+ }
+ return result;
+}
+
+/**
+ * Find the nodes that needs to be updated.
+ *
+ * The nodes that require updated are added to the r_nodes_to_update parameter.
+ * Will fill in r_visited_polygons with polygons that are owned by nodes that do not require
+ * updates.
+ *
+ * returns if there were any nodes found (true).
+ */
+static bool find_nodes_to_update(PBVH *pbvh, Vector<PBVHNode *> &r_nodes_to_update)
+{
+ int64_t nodes_to_update_len = count_nodes_to_update(pbvh);
+ if (nodes_to_update_len == 0) {
+ return false;
+ }
+
+ r_nodes_to_update.reserve(nodes_to_update_len);
+
+ for (int n = 0; n < pbvh->totnode; n++) {
+ PBVHNode *node = &pbvh->nodes[n];
+ if (!should_pixels_be_updated(node)) {
+ continue;
+ }
+ r_nodes_to_update.append(node);
+ node->flag = static_cast<PBVHNodeFlags>(node->flag | PBVH_RebuildPixels);
+
+ if (node->pixels.node_data == nullptr) {
+ NodeData *node_data = MEM_new<NodeData>(__func__);
+ node->pixels.node_data = node_data;
+ }
+ else {
+ NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
+ node_data->clear_data();
+ }
+ }
+
+ return true;
+}
+
+static void apply_watertight_check(PBVH *pbvh, Image *image, ImageUser *image_user)
+{
+ ImageUser watertight = *image_user;
+ LISTBASE_FOREACH (ImageTile *, tile_data, &image->tiles) {
+ image::ImageTileWrapper image_tile(tile_data);
+ watertight.tile = image_tile.get_tile_number();
+ ImBuf *image_buffer = BKE_image_acquire_ibuf(image, &watertight, nullptr);
+ if (image_buffer == nullptr) {
+ continue;
+ }
+ for (int n = 0; n < pbvh->totnode; n++) {
+ PBVHNode *node = &pbvh->nodes[n];
+ if ((node->flag & PBVH_Leaf) == 0) {
+ continue;
+ }
+ NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
+ UDIMTilePixels *tile_node_data = node_data->find_tile_data(image_tile);
+ if (tile_node_data == nullptr) {
+ continue;
+ }
+
+ for (PackedPixelRow &pixel_row : tile_node_data->pixel_rows) {
+ int pixel_offset = pixel_row.start_image_coordinate.y * image_buffer->x +
+ pixel_row.start_image_coordinate.x;
+ for (int x = 0; x < pixel_row.num_pixels; x++) {
+ if (image_buffer->rect_float) {
+ copy_v4_fl(&image_buffer->rect_float[pixel_offset * 4], 1.0);
+ }
+ if (image_buffer->rect) {
+ uint8_t *dest = static_cast<uint8_t *>(
+ static_cast<void *>(&image_buffer->rect[pixel_offset]));
+ copy_v4_uchar(dest, 255);
+ }
+ pixel_offset += 1;
+ }
+ }
+ }
+ BKE_image_release_ibuf(image, image_buffer, nullptr);
+ }
+ BKE_image_partial_update_mark_full_update(image);
+}
+
+static void update_pixels(PBVH *pbvh,
+ const struct MLoop *mloop,
+ struct CustomData *ldata,
+ struct Image *image,
+ struct ImageUser *image_user)
+{
+ Vector<PBVHNode *> nodes_to_update;
+
+ if (!find_nodes_to_update(pbvh, nodes_to_update)) {
+ return;
+ }
+
+ MLoopUV *ldata_uv = static_cast<MLoopUV *>(CustomData_get_layer(ldata, CD_MLOOPUV));
+ if (ldata_uv == nullptr) {
+ return;
+ }
+
+ for (PBVHNode *node : nodes_to_update) {
+ NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
+ init_triangles(pbvh, node, node_data, mloop);
+ }
+
+ EncodePixelsUserData user_data;
+ user_data.pbvh = pbvh;
+ user_data.image = image;
+ user_data.image_user = image_user;
+ user_data.ldata_uv = ldata_uv;
+ user_data.nodes = &nodes_to_update;
+
+ TaskParallelSettings settings;
+ BKE_pbvh_parallel_range_settings(&settings, true, nodes_to_update.size());
+ BLI_task_parallel_range(0, nodes_to_update.size(), &user_data, do_encode_pixels, &settings);
+ if (USE_WATERTIGHT_CHECK) {
+ apply_watertight_check(pbvh, image, image_user);
+ }
+
+ /* Clear the UpdatePixels flag. */
+ for (PBVHNode *node : nodes_to_update) {
+ node->flag = static_cast<PBVHNodeFlags>(node->flag & ~PBVH_RebuildPixels);
+ }
+
+//#define DO_PRINT_STATISTICS
+#ifdef DO_PRINT_STATISTICS
+ /* Print some statistics about compression ratio. */
+ {
+ int64_t compressed_data_len = 0;
+ int64_t num_pixels = 0;
+ for (int n = 0; n < pbvh->totnode; n++) {
+ PBVHNode *node = &pbvh->nodes[n];
+ if ((node->flag & PBVH_Leaf) == 0) {
+ continue;
+ }
+ NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
+ compressed_data_len += node_data->triangles.mem_size();
+ for (const UDIMTilePixels &tile_data : node_data->tiles) {
+ compressed_data_len += tile_data.encoded_pixels.size() * sizeof(PackedPixelRow);
+ for (const PackedPixelRow &encoded_pixels : tile_data.encoded_pixels) {
+ num_pixels += encoded_pixels.num_pixels;
+ }
+ }
+ }
+ printf("Encoded %lld pixels in %lld bytes (%f bytes per pixel)\n",
+ num_pixels,
+ compressed_data_len,
+ float(compressed_data_len) / num_pixels);
+ }
+#endif
+}
+
+NodeData &BKE_pbvh_pixels_node_data_get(PBVHNode &node)
+{
+ BLI_assert(node.pixels.node_data != nullptr);
+ NodeData *node_data = static_cast<NodeData *>(node.pixels.node_data);
+ return *node_data;
+}
+
+void BKE_pbvh_pixels_mark_image_dirty(PBVHNode &node, Image &image, ImageUser &image_user)
+{
+ BLI_assert(node.pixels.node_data != nullptr);
+ NodeData *node_data = static_cast<NodeData *>(node.pixels.node_data);
+ if (node_data->flags.dirty) {
+ ImageUser local_image_user = image_user;
+ LISTBASE_FOREACH (ImageTile *, tile, &image.tiles) {
+ image::ImageTileWrapper image_tile(tile);
+ local_image_user.tile = image_tile.get_tile_number();
+ ImBuf *image_buffer = BKE_image_acquire_ibuf(&image, &local_image_user, nullptr);
+ if (image_buffer == nullptr) {
+ continue;
+ }
+
+ node_data->mark_region(image, image_tile, *image_buffer);
+ BKE_image_release_ibuf(&image, image_buffer, nullptr);
+ }
+ node_data->flags.dirty = false;
+ }
+}
+
+} // namespace blender::bke::pbvh::pixels
+
+extern "C" {
+using namespace blender::bke::pbvh::pixels;
+
+void BKE_pbvh_build_pixels(PBVH *pbvh,
+ const struct MLoop *mloop,
+ struct CustomData *ldata,
+ struct Image *image,
+ struct ImageUser *image_user)
+{
+ update_pixels(pbvh, mloop, ldata, image, image_user);
+}
+
+void pbvh_pixels_free(PBVHNode *node)
+{
+ NodeData *node_data = static_cast<NodeData *>(node->pixels.node_data);
+ MEM_delete(node_data);
+ node->pixels.node_data = nullptr;
+}
+}
diff --git a/source/blender/editors/include/ED_sculpt.h b/source/blender/editors/include/ED_sculpt.h
index 3100c135db4..54d67c50d5c 100644
--- a/source/blender/editors/include/ED_sculpt.h
+++ b/source/blender/editors/include/ED_sculpt.h
@@ -17,6 +17,9 @@ struct UndoType;
struct ViewContext;
struct bContext;
struct rcti;
+struct wmMsgSubscribeKey;
+struct wmMsgSubscribeValue;
+struct wmRegionMessageSubscribeParams;
/* sculpt.c */
diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt
index b89cbcf87fa..abdae5c44f9 100644
--- a/source/blender/editors/sculpt_paint/CMakeLists.txt
+++ b/source/blender/editors/sculpt_paint/CMakeLists.txt
@@ -72,6 +72,7 @@ set(SRC
sculpt_multiplane_scrape.c
sculpt_ops.c
sculpt_paint_color.c
+ sculpt_paint_image.cc
sculpt_pose.c
sculpt_smooth.c
sculpt_transform.c
diff --git a/source/blender/editors/sculpt_paint/sculpt.c b/source/blender/editors/sculpt_paint/sculpt.c
index 24c5a9fce52..bbbed32e316 100644
--- a/source/blender/editors/sculpt_paint/sculpt.c
+++ b/source/blender/editors/sculpt_paint/sculpt.c
@@ -2188,7 +2188,8 @@ void SCULPT_calc_area_normal_and_center(
static float brush_strength(const Sculpt *sd,
const StrokeCache *cache,
const float feather,
- const UnifiedPaintSettings *ups)
+ const UnifiedPaintSettings *ups,
+ const PaintModeSettings *UNUSED(paint_mode_settings))
{
const Scene *scene = cache->vc->scene;
const Brush *brush = BKE_paint_brush((Paint *)&sd->paint);
@@ -2750,6 +2751,41 @@ static void update_brush_local_mat(Sculpt *sd, Object *ob)
/** \} */
/* -------------------------------------------------------------------- */
+/** \name Texture painting
+ * \{ */
+
+static bool sculpt_needs_pbvh_pixels(PaintModeSettings *paint_mode_settings,
+ const Brush *brush,
+ Object *ob)
+{
+ if (brush->sculpt_tool == SCULPT_TOOL_PAINT && U.experimental.use_sculpt_texture_paint) {
+ Image *image;
+ ImageUser *image_user;
+ return SCULPT_paint_image_canvas_get(paint_mode_settings, ob, &image, &image_user);
+ }
+
+ return false;
+}
+
+static void sculpt_pbvh_update_pixels(PaintModeSettings *paint_mode_settings,
+ SculptSession *ss,
+ Object *ob)
+{
+ BLI_assert(ob->type == OB_MESH);
+ Mesh *mesh = (Mesh *)ob->data;
+
+ Image *image;
+ ImageUser *image_user;
+ if (!SCULPT_paint_image_canvas_get(paint_mode_settings, ob, &image, &image_user)) {
+ return;
+ }
+
+ BKE_pbvh_build_pixels(ss->pbvh, mesh->mloop, &mesh->ldata, image, image_user);
+}
+
+/** \} */
+
+/* -------------------------------------------------------------------- */
/** \name Generic Brush Plane & Symmetry Utilities
* \{ */
@@ -3075,7 +3111,8 @@ void SCULPT_vertcos_to_key(Object *ob, KeyBlock *kb, const float (*vertCos)[3])
static void sculpt_topology_update(Sculpt *sd,
Object *ob,
Brush *brush,
- UnifiedPaintSettings *UNUSED(ups))
+ UnifiedPaintSettings *UNUSED(ups),
+ PaintModeSettings *UNUSED(paint_mode_settings))
{
SculptSession *ss = ob->sculpt;
@@ -3170,7 +3207,11 @@ static void do_brush_action_task_cb(void *__restrict userdata,
}
}
-static void do_brush_action(Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSettings *ups)
+static void do_brush_action(Sculpt *sd,
+ Object *ob,
+ Brush *brush,
+ UnifiedPaintSettings *ups,
+ PaintModeSettings *paint_mode_settings)
{
SculptSession *ss = ob->sculpt;
int totnode;
@@ -3209,6 +3250,10 @@ static void do_brush_action(Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSe
nodes = sculpt_pbvh_gather_generic(ob, sd, brush, use_original, radius_scale, &totnode);
}
+ if (sculpt_needs_pbvh_pixels(paint_mode_settings, brush, ob)) {
+ sculpt_pbvh_update_pixels(paint_mode_settings, ss, ob);
+ }
+
/* Draw Face Sets in draw mode makes a single undo push, in alt-smooth mode deforms the
* vertices and uses regular coords undo. */
/* It also assigns the paint_face_set here as it needs to be done regardless of the stroke type
@@ -3399,7 +3444,7 @@ static void do_brush_action(Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSe
SCULPT_do_displacement_smear_brush(sd, ob, nodes, totnode);
break;
case SCULPT_TOOL_PAINT:
- SCULPT_do_paint_brush(sd, ob, nodes, totnode);
+ SCULPT_do_paint_brush(paint_mode_settings, sd, ob, nodes, totnode);
break;
case SCULPT_TOOL_SMEAR:
SCULPT_do_smear_brush(sd, ob, nodes, totnode);
@@ -3704,10 +3749,18 @@ void SCULPT_cache_calc_brushdata_symm(StrokeCache *cache,
}
}
-typedef void (*BrushActionFunc)(Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSettings *ups);
+typedef void (*BrushActionFunc)(Sculpt *sd,
+ Object *ob,
+ Brush *brush,
+ UnifiedPaintSettings *ups,
+ PaintModeSettings *paint_mode_settings);
-static void do_tiled(
- Sculpt *sd, Object *ob, Brush *brush, UnifiedPaintSettings *ups, BrushActionFunc action)
+static void do_tiled(Sculpt *sd,
+ Object *ob,
+ Brush *brush,
+ UnifiedPaintSettings *ups,
+ PaintModeSettings *paint_mode_settings,
+ BrushActionFunc action)
{
SculptSession *ss = ob->sculpt;
StrokeCache *cache = ss->cache;
@@ -3741,7 +3794,7 @@ static void do_tiled(
/* First do the "un-tiled" position to initialize the stroke for this location. */
cache->tile_pass = 0;
- action(sd, ob, brush, ups);
+ action(sd, ob, brush, ups, paint_mode_settings);
/* Now do it for all the tiles. */
copy_v3_v3_int(cur, start);
@@ -3760,7 +3813,7 @@ static void do_tiled(
cache->plane_offset[dim] = cur[dim] * step[dim];
cache->initial_location[dim] = cur[dim] * step[dim] + original_initial_location[dim];
}
- action(sd, ob, brush, ups);
+ action(sd, ob, brush, ups, paint_mode_settings);
}
}
}
@@ -3770,6 +3823,7 @@ static void do_radial_symmetry(Sculpt *sd,
Object *ob,
Brush *brush,
UnifiedPaintSettings *ups,
+ PaintModeSettings *paint_mode_settings,
BrushActionFunc action,
const char symm,
const int axis,
@@ -3781,7 +3835,7 @@ static void do_radial_symmetry(Sculpt *sd,
const float angle = 2.0f * M_PI * i / sd->radial_symm[axis - 'X'];
ss->cache->radial_symmetry_pass = i;
SCULPT_cache_calc_brushdata_symm(ss->cache, symm, axis, angle);
- do_tiled(sd, ob, brush, ups, action);
+ do_tiled(sd, ob, brush, ups, paint_mode_settings, action);
}
}
@@ -3803,7 +3857,8 @@ static void sculpt_fix_noise_tear(Sculpt *sd, Object *ob)
static void do_symmetrical_brush_actions(Sculpt *sd,
Object *ob,
BrushActionFunc action,
- UnifiedPaintSettings *ups)
+ UnifiedPaintSettings *ups,
+ PaintModeSettings *paint_mode_settings)
{
Brush *brush = BKE_paint_brush(&sd->paint);
SculptSession *ss = ob->sculpt;
@@ -3812,7 +3867,7 @@ static void do_symmetrical_brush_actions(Sculpt *sd,
float feather = calc_symmetry_feather(sd, ss->cache);
- cache->bstrength = brush_strength(sd, cache, feather, ups);
+ cache->bstrength = brush_strength(sd, cache, feather, ups, paint_mode_settings);
cache->symmetry = symm;
/* `symm` is a bit combination of XYZ -
@@ -3825,11 +3880,11 @@ static void do_symmetrical_brush_actions(Sculpt *sd,
cache->radial_symmetry_pass = 0;
SCULPT_cache_calc_brushdata_symm(cache, i, 0, 0);
- do_tiled(sd, ob, brush, ups, action);
+ do_tiled(sd, ob, brush, ups, paint_mode_settings, action);
- do_radial_symmetry(sd, ob, brush, ups, action, i, 'X', feather);
- do_radial_symmetry(sd, ob, brush, ups, action, i, 'Y', feather);
- do_radial_symmetry(sd, ob, brush, ups, action, i, 'Z', feather);
+ do_radial_symmetry(sd, ob, brush, ups, paint_mode_settings, action, i, 'X', feather);
+ do_radial_symmetry(sd, ob, brush, ups, paint_mode_settings, action, i, 'Y', feather);
+ do_radial_symmetry(sd, ob, brush, ups, paint_mode_settings, action, i, 'Z', feather);
}
}
@@ -4609,7 +4664,8 @@ static bool sculpt_needs_connectivity_info(const Sculpt *sd,
SCULPT_TOOL_NEEDS_COLOR(brush->sculpt_tool) ||
(brush->sculpt_tool == SCULPT_TOOL_CLOTH) || (brush->sculpt_tool == SCULPT_TOOL_SMEAR) ||
(brush->sculpt_tool == SCULPT_TOOL_DRAW_FACE_SETS) ||
- (brush->sculpt_tool == SCULPT_TOOL_DISPLACEMENT_SMEAR));
+ (brush->sculpt_tool == SCULPT_TOOL_DISPLACEMENT_SMEAR) ||
+ (brush->sculpt_tool == SCULPT_TOOL_PAINT));
}
void SCULPT_stroke_modifiers_check(const bContext *C, Object *ob, const Brush *brush)
@@ -5057,6 +5113,15 @@ void SCULPT_flush_update_step(bContext *C, SculptUpdateType update_flags)
multires_mark_as_modified(depsgraph, ob, MULTIRES_COORDS_MODIFIED);
}
+ if ((update_flags & SCULPT_UPDATE_IMAGE) != 0) {
+ ED_region_tag_redraw(region);
+ if (update_flags == SCULPT_UPDATE_IMAGE) {
+ /* Early exit when only need to update the images. We don't want to tag any geometry updates
+ * that would rebuilt the PBVH. */
+ return;
+ }
+ }
+
DEG_id_tag_update(&ob->id, ID_RECALC_SHADING);
/* Only current viewport matters, slower update for all viewports will
@@ -5136,6 +5201,16 @@ void SCULPT_flush_update_done(const bContext *C, Object *ob, SculptUpdateType up
}
}
}
+
+ if (update_flags & SCULPT_UPDATE_IMAGE) {
+ LISTBASE_FOREACH (ScrArea *, area, &screen->areabase) {
+ SpaceLink *sl = area->spacedata.first;
+ if (sl->spacetype != SPACE_IMAGE) {
+ continue;
+ }
+ ED_area_tag_redraw_regiontype(area, RGN_TYPE_WINDOW);
+ }
+ }
}
if (update_flags & SCULPT_UPDATE_COORDS) {
@@ -5227,6 +5302,7 @@ static void sculpt_stroke_update_step(bContext *C,
Object *ob = CTX_data_active_object(C);
SculptSession *ss = ob->sculpt;
const Brush *brush = BKE_paint_brush(&sd->paint);
+ ToolSettings *tool_settings = CTX_data_tool_settings(C);
SCULPT_stroke_modifiers_check(C, ob, brush);
sculpt_update_cache_variants(C, sd, ob, itemptr);
@@ -5246,10 +5322,10 @@ static void sculpt_stroke_update_step(bContext *C,
}
if (SCULPT_stroke_is_dynamic_topology(ss, brush)) {
- do_symmetrical_brush_actions(sd, ob, sculpt_topology_update, ups);
+ do_symmetrical_brush_actions(sd, ob, sculpt_topology_update, ups, &tool_settings->paint_mode);
}
- do_symmetrical_brush_actions(sd, ob, do_brush_action, ups);
+ do_symmetrical_brush_actions(sd, ob, do_brush_action, ups, &tool_settings->paint_mode);
sculpt_combine_proxies(sd, ob);
/* Hack to fix noise texture tearing mesh. */
@@ -5280,7 +5356,12 @@ static void sculpt_stroke_update_step(bContext *C,
SCULPT_flush_update_step(C, SCULPT_UPDATE_MASK);
}
else if (ELEM(brush->sculpt_tool, SCULPT_TOOL_PAINT, SCULPT_TOOL_SMEAR)) {
- SCULPT_flush_update_step(C, SCULPT_UPDATE_COLOR);
+ if (SCULPT_use_image_paint_brush(&tool_settings->paint_mode, ob)) {
+ SCULPT_flush_update_step(C, SCULPT_UPDATE_IMAGE);
+ }
+ else {
+ SCULPT_flush_update_step(C, SCULPT_UPDATE_COLOR);
+ }
}
else {
SCULPT_flush_update_step(C, SCULPT_UPDATE_COORDS);
@@ -5302,6 +5383,7 @@ static void sculpt_stroke_done(const bContext *C, struct PaintStroke *UNUSED(str
Object *ob = CTX_data_active_object(C);
SculptSession *ss = ob->sculpt;
Sculpt *sd = CTX_data_tool_settings(C)->sculpt;
+ ToolSettings *tool_settings = CTX_data_tool_settings(C);
/* Finished. */
if (!ss->cache) {
@@ -5335,6 +5417,11 @@ static void sculpt_stroke_done(const bContext *C, struct PaintStroke *UNUSED(str
if (brush->sculpt_tool == SCULPT_TOOL_MASK) {
SCULPT_flush_update_done(C, ob, SCULPT_UPDATE_MASK);
}
+ else if (brush->sculpt_tool == SCULPT_TOOL_PAINT) {
+ if (SCULPT_use_image_paint_brush(&tool_settings->paint_mode, ob)) {
+ SCULPT_flush_update_done(C, ob, SCULPT_UPDATE_IMAGE);
+ }
+ }
else {
SCULPT_flush_update_done(C, ob, SCULPT_UPDATE_COORDS);
}
diff --git a/source/blender/editors/sculpt_paint/sculpt_intern.h b/source/blender/editors/sculpt_paint/sculpt_intern.h
index 466432a35ec..3839c0e71e4 100644
--- a/source/blender/editors/sculpt_paint/sculpt_intern.h
+++ b/source/blender/editors/sculpt_paint/sculpt_intern.h
@@ -11,11 +11,13 @@
#include "DNA_key_types.h"
#include "DNA_listBase.h"
#include "DNA_meshdata_types.h"
+#include "DNA_scene_types.h"
#include "DNA_vec_types.h"
#include "BKE_paint.h"
#include "BKE_pbvh.h"
#include "BLI_bitmap.h"
+#include "BLI_compiler_attrs.h"
#include "BLI_compiler_compat.h"
#include "BLI_gsqueue.h"
#include "BLI_threads.h"
@@ -25,12 +27,13 @@ extern "C" {
#endif
struct AutomaskingCache;
+struct Image;
+struct ImageUser;
struct KeyBlock;
struct Object;
struct SculptUndoNode;
struct bContext;
-
-enum ePaintSymmetryFlags;
+struct PaintModeSettings;
/* Updates */
@@ -43,6 +46,7 @@ typedef enum SculptUpdateType {
SCULPT_UPDATE_MASK = 1 << 1,
SCULPT_UPDATE_VISIBILITY = 1 << 2,
SCULPT_UPDATE_COLOR = 1 << 3,
+ SCULPT_UPDATE_IMAGE = 1 << 4,
} SculptUpdateType;
typedef struct SculptCursorGeometryInfo {
@@ -1626,7 +1630,29 @@ void SCULPT_multiplane_scrape_preview_draw(uint gpuattr,
void SCULPT_do_draw_face_sets_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode);
/* Paint Brush. */
-void SCULPT_do_paint_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode);
+void SCULPT_do_paint_brush(struct PaintModeSettings *paint_mode_settings,
+ Sculpt *sd,
+ Object *ob,
+ PBVHNode **nodes,
+ int totnode) ATTR_NONNULL();
+
+/**
+ * @brief Get the image canvas for painting on the given object.
+ *
+ * @return #true if an image is found. The #r_image and #r_image_user fields are filled with the
+ * image and image user. Returns false when the image isn't found. In the later case the r_image
+ * and r_image_user are set to nullptr/NULL.
+ */
+bool SCULPT_paint_image_canvas_get(struct PaintModeSettings *paint_mode_settings,
+ struct Object *ob,
+ struct Image **r_image,
+ struct ImageUser **r_image_user) ATTR_NONNULL();
+void SCULPT_do_paint_brush_image(struct PaintModeSettings *paint_mode_settings,
+ Sculpt *sd,
+ Object *ob,
+ PBVHNode **nodes,
+ int totnode) ATTR_NONNULL();
+bool SCULPT_use_image_paint_brush(struct PaintModeSettings *settings, Object *ob) ATTR_NONNULL();
/* Smear Brush. */
void SCULPT_do_smear_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode);
diff --git a/source/blender/editors/sculpt_paint/sculpt_paint_color.c b/source/blender/editors/sculpt_paint/sculpt_paint_color.c
index e7a713efa14..7a8a6e8e484 100644
--- a/source/blender/editors/sculpt_paint/sculpt_paint_color.c
+++ b/source/blender/editors/sculpt_paint/sculpt_paint_color.c
@@ -240,8 +240,14 @@ static void sample_wet_paint_reduce(const void *__restrict UNUSED(userdata),
add_v4_v4(join->color, swptd->color);
}
-void SCULPT_do_paint_brush(Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode)
+void SCULPT_do_paint_brush(
+ PaintModeSettings *paint_mode_settings, Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode)
{
+ if (SCULPT_use_image_paint_brush(paint_mode_settings, ob)) {
+ SCULPT_do_paint_brush_image(paint_mode_settings, sd, ob, nodes, totnode);
+ return;
+ }
+
Brush *brush = BKE_paint_brush(&sd->paint);
SculptSession *ss = ob->sculpt;
diff --git a/source/blender/editors/sculpt_paint/sculpt_paint_image.cc b/source/blender/editors/sculpt_paint/sculpt_paint_image.cc
new file mode 100644
index 00000000000..082ff6260c2
--- /dev/null
+++ b/source/blender/editors/sculpt_paint/sculpt_paint_image.cc
@@ -0,0 +1,432 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright 2022 Blender Foundation. All rights reserved. */
+
+#include "DNA_image_types.h"
+#include "DNA_material_types.h"
+#include "DNA_mesh_types.h"
+#include "DNA_node_types.h"
+#include "DNA_object_types.h"
+
+#include "ED_paint.h"
+#include "ED_uvedit.h"
+
+#include "BLI_math.h"
+#include "BLI_math_color_blend.h"
+#include "BLI_task.h"
+
+#include "IMB_colormanagement.h"
+#include "IMB_imbuf.h"
+
+#include "BKE_brush.h"
+#include "BKE_image_wrappers.hh"
+#include "BKE_material.h"
+#include "BKE_pbvh.h"
+#include "BKE_pbvh_pixels.hh"
+
+#include "bmesh.h"
+
+#include "NOD_shader.h"
+
+#include "sculpt_intern.h"
+
+namespace blender::ed::sculpt_paint::paint::image {
+
+using namespace blender::bke::pbvh::pixels;
+using namespace blender::bke::image;
+
+struct ImageData {
+ Image *image = nullptr;
+ ImageUser *image_user = nullptr;
+
+ ~ImageData()
+ {
+ }
+
+ static bool init_active_image(Object *ob,
+ ImageData *r_image_data,
+ PaintModeSettings *paint_mode_settings)
+ {
+ return BKE_paint_canvas_image_get(
+ paint_mode_settings, ob, &r_image_data->image, &r_image_data->image_user);
+ }
+};
+
+struct TexturePaintingUserData {
+ Object *ob;
+ Brush *brush;
+ PBVHNode **nodes;
+ ImageData image_data;
+};
+
+/** Reading and writing to image buffer with 4 float channels. */
+class ImageBufferFloat4 {
+ private:
+ int pixel_offset;
+
+ public:
+ void set_image_position(ImBuf *image_buffer, ushort2 image_pixel_position)
+ {
+ pixel_offset = int(image_pixel_position.y) * image_buffer->x + int(image_pixel_position.x);
+ }
+
+ void next_pixel()
+ {
+ pixel_offset += 1;
+ }
+
+ float4 read_pixel(ImBuf *image_buffer) const
+ {
+ return &image_buffer->rect_float[pixel_offset * 4];
+ }
+
+ void write_pixel(ImBuf *image_buffer, const float4 pixel_data) const
+ {
+ copy_v4_v4(&image_buffer->rect_float[pixel_offset * 4], pixel_data);
+ }
+
+ const char *get_colorspace_name(ImBuf *image_buffer)
+ {
+ return IMB_colormanagement_get_float_colorspace(image_buffer);
+ }
+};
+
+/** Reading and writing to image buffer with 4 byte channels. */
+class ImageBufferByte4 {
+ private:
+ int pixel_offset;
+
+ public:
+ void set_image_position(ImBuf *image_buffer, ushort2 image_pixel_position)
+ {
+ pixel_offset = int(image_pixel_position.y) * image_buffer->x + int(image_pixel_position.x);
+ }
+
+ void next_pixel()
+ {
+ pixel_offset += 1;
+ }
+
+ float4 read_pixel(ImBuf *image_buffer) const
+ {
+ float4 result;
+ rgba_uchar_to_float(result,
+ static_cast<const uchar *>(
+ static_cast<const void *>(&(image_buffer->rect[pixel_offset]))));
+ return result;
+ }
+
+ void write_pixel(ImBuf *image_buffer, const float4 pixel_data) const
+ {
+ rgba_float_to_uchar(
+ static_cast<uchar *>(static_cast<void *>(&image_buffer->rect[pixel_offset])), pixel_data);
+ }
+
+ const char *get_colorspace_name(ImBuf *image_buffer)
+ {
+ return IMB_colormanagement_get_rect_colorspace(image_buffer);
+ }
+};
+
+template<typename ImageBuffer> class PaintingKernel {
+ ImageBuffer image_accessor;
+
+ SculptSession *ss;
+ const Brush *brush;
+ const int thread_id;
+ const MVert *mvert;
+
+ float4 brush_color;
+ float brush_strength;
+
+ SculptBrushTestFn brush_test_fn;
+ SculptBrushTest test;
+ /* Pointer to the last used image buffer to detect when buffers are switched. */
+ void *last_used_image_buffer_ptr = nullptr;
+ const char *last_used_color_space = nullptr;
+
+ public:
+ explicit PaintingKernel(SculptSession *ss,
+ const Brush *brush,
+ const int thread_id,
+ const MVert *mvert)
+ : ss(ss), brush(brush), thread_id(thread_id), mvert(mvert)
+ {
+ init_brush_strength();
+ init_brush_test();
+ }
+
+ bool paint(const Triangles &triangles, const PackedPixelRow &pixel_row, ImBuf *image_buffer)
+ {
+ image_accessor.set_image_position(image_buffer, pixel_row.start_image_coordinate);
+ const TrianglePaintInput triangle = triangles.get_paint_input(pixel_row.triangle_index);
+ float3 pixel_pos = get_start_pixel_pos(triangle, pixel_row);
+ const float3 delta_pixel_pos = get_delta_pixel_pos(triangle, pixel_row, pixel_pos);
+ bool pixels_painted = false;
+ for (int x = 0; x < pixel_row.num_pixels; x++) {
+ if (!brush_test_fn(&test, pixel_pos)) {
+ pixel_pos += delta_pixel_pos;
+ image_accessor.next_pixel();
+ continue;
+ }
+
+ float4 color = image_accessor.read_pixel(image_buffer);
+ const float3 normal(0.0f, 0.0f, 0.0f);
+ const float3 face_normal(0.0f, 0.0f, 0.0f);
+ const float mask = 0.0f;
+ const float falloff_strength = SCULPT_brush_strength_factor(
+ ss, brush, pixel_pos, sqrtf(test.dist), normal, face_normal, mask, 0, thread_id);
+ float4 paint_color = brush_color * falloff_strength * brush_strength;
+ float4 buffer_color;
+ blend_color_mix_float(buffer_color, color, paint_color);
+ buffer_color *= brush->alpha;
+ IMB_blend_color_float(color, color, buffer_color, static_cast<IMB_BlendMode>(brush->blend));
+ image_accessor.write_pixel(image_buffer, color);
+ pixels_painted = true;
+
+ image_accessor.next_pixel();
+ pixel_pos += delta_pixel_pos;
+ }
+ return pixels_painted;
+ }
+
+ void init_brush_color(ImBuf *image_buffer)
+ {
+ const char *to_colorspace = image_accessor.get_colorspace_name(image_buffer);
+ if (last_used_color_space == to_colorspace) {
+ return;
+ }
+ copy_v3_v3(brush_color,
+ ss->cache->invert ? BKE_brush_secondary_color_get(ss->scene, brush) :
+ BKE_brush_color_get(ss->scene, brush));
+ /* NOTE: Brush colors are stored in sRGB. We use math color to follow other areas that
+ * use brush colors. From there on we use IMB_colormanagement to convert the brush color to the
+ * colorspace of the texture. This isn't ideal, but would need more refactoring to make sure
+ * that brush colors are stored in scene linear by default. */
+ srgb_to_linearrgb_v3_v3(brush_color, brush_color);
+ brush_color[3] = 1.0f;
+
+ const char *from_colorspace = IMB_colormanagement_role_colorspace_name_get(
+ COLOR_ROLE_SCENE_LINEAR);
+ ColormanageProcessor *cm_processor = IMB_colormanagement_colorspace_processor_new(
+ from_colorspace, to_colorspace);
+ IMB_colormanagement_processor_apply_v4(cm_processor, brush_color);
+ IMB_colormanagement_processor_free(cm_processor);
+ last_used_color_space = to_colorspace;
+ }
+
+ private:
+ void init_brush_strength()
+ {
+ brush_strength = ss->cache->bstrength;
+ }
+ void init_brush_test()
+ {
+ brush_test_fn = SCULPT_brush_test_init_with_falloff_shape(ss, &test, brush->falloff_shape);
+ }
+
+ /**
+ * Extract the starting pixel position from the given encoded_pixels belonging to the triangle.
+ */
+ float3 get_start_pixel_pos(const TrianglePaintInput &triangle,
+ const PackedPixelRow &encoded_pixels) const
+ {
+ return init_pixel_pos(triangle, encoded_pixels.start_barycentric_coord);
+ }
+
+ /**
+ * Extract the delta pixel position that will be used to advance a Pixel instance to the next
+ * pixel.
+ */
+ float3 get_delta_pixel_pos(const TrianglePaintInput &triangle,
+ const PackedPixelRow &encoded_pixels,
+ const float3 &start_pixel) const
+ {
+ float3 result = init_pixel_pos(
+ triangle, encoded_pixels.start_barycentric_coord + triangle.delta_barycentric_coord_u);
+ return result - start_pixel;
+ }
+
+ float3 init_pixel_pos(const TrianglePaintInput &triangle,
+ const float2 &barycentric_weights) const
+ {
+ const int3 &vert_indices = triangle.vert_indices;
+ float3 result;
+ const float3 barycentric(barycentric_weights.x,
+ barycentric_weights.y,
+ 1.0f - barycentric_weights.x - barycentric_weights.y);
+ interp_v3_v3v3v3(result,
+ mvert[vert_indices[0]].co,
+ mvert[vert_indices[1]].co,
+ mvert[vert_indices[2]].co,
+ barycentric);
+ return result;
+ }
+};
+
+static std::vector<bool> init_triangle_brush_test(SculptSession *ss,
+ Triangles &triangles,
+ const MVert *mvert)
+{
+ std::vector<bool> brush_test(triangles.size());
+ SculptBrushTest test;
+ SCULPT_brush_test_init(ss, &test);
+ float3 brush_min_bounds(test.location[0] - test.radius,
+ test.location[1] - test.radius,
+ test.location[2] - test.radius);
+ float3 brush_max_bounds(test.location[0] + test.radius,
+ test.location[1] + test.radius,
+ test.location[2] + test.radius);
+ for (int triangle_index = 0; triangle_index < triangles.size(); triangle_index++) {
+ TrianglePaintInput &triangle = triangles.get_paint_input(triangle_index);
+
+ float3 triangle_min_bounds(mvert[triangle.vert_indices[0]].co);
+ float3 triangle_max_bounds(triangle_min_bounds);
+ for (int i = 1; i < 3; i++) {
+ const float3 &pos = mvert[triangle.vert_indices[i]].co;
+ triangle_min_bounds.x = min_ff(triangle_min_bounds.x, pos.x);
+ triangle_min_bounds.y = min_ff(triangle_min_bounds.y, pos.y);
+ triangle_min_bounds.z = min_ff(triangle_min_bounds.z, pos.z);
+ triangle_max_bounds.x = max_ff(triangle_max_bounds.x, pos.x);
+ triangle_max_bounds.y = max_ff(triangle_max_bounds.y, pos.y);
+ triangle_max_bounds.z = max_ff(triangle_max_bounds.z, pos.z);
+ }
+ brush_test[triangle_index] = isect_aabb_aabb_v3(
+ brush_min_bounds, brush_max_bounds, triangle_min_bounds, triangle_max_bounds);
+ }
+ return brush_test;
+}
+
+static void do_paint_pixels(void *__restrict userdata,
+ const int n,
+ const TaskParallelTLS *__restrict tls)
+{
+ TexturePaintingUserData *data = static_cast<TexturePaintingUserData *>(userdata);
+ Object *ob = data->ob;
+ SculptSession *ss = ob->sculpt;
+ const Brush *brush = data->brush;
+ PBVHNode *node = data->nodes[n];
+
+ NodeData &node_data = BKE_pbvh_pixels_node_data_get(*node);
+ const int thread_id = BLI_task_parallel_thread_id(tls);
+ MVert *mvert = SCULPT_mesh_deformed_mverts_get(ss);
+
+ std::vector<bool> brush_test = init_triangle_brush_test(ss, node_data.triangles, mvert);
+
+ PaintingKernel<ImageBufferFloat4> kernel_float4(ss, brush, thread_id, mvert);
+ PaintingKernel<ImageBufferByte4> kernel_byte4(ss, brush, thread_id, mvert);
+
+ ImageUser image_user = *data->image_data.image_user;
+ bool pixels_updated = false;
+ for (UDIMTilePixels &tile_data : node_data.tiles) {
+ LISTBASE_FOREACH (ImageTile *, tile, &data->image_data.image->tiles) {
+ ImageTileWrapper image_tile(tile);
+ if (image_tile.get_tile_number() == tile_data.tile_number) {
+ image_user.tile = image_tile.get_tile_number();
+
+ ImBuf *image_buffer = BKE_image_acquire_ibuf(data->image_data.image, &image_user, nullptr);
+ if (image_buffer == nullptr) {
+ continue;
+ }
+
+ if (image_buffer->rect_float != nullptr) {
+ kernel_float4.init_brush_color(image_buffer);
+ }
+ else {
+ kernel_float4.init_brush_color(image_buffer);
+ }
+
+ for (const PackedPixelRow &pixel_row : tile_data.pixel_rows) {
+ if (!brush_test[pixel_row.triangle_index]) {
+ continue;
+ }
+ bool pixels_painted = false;
+ if (image_buffer->rect_float != nullptr) {
+ pixels_painted = kernel_float4.paint(node_data.triangles, pixel_row, image_buffer);
+ }
+ else {
+ pixels_painted = kernel_byte4.paint(node_data.triangles, pixel_row, image_buffer);
+ }
+
+ if (pixels_painted) {
+ tile_data.mark_dirty(pixel_row);
+ }
+ }
+
+ BKE_image_release_ibuf(data->image_data.image, image_buffer, nullptr);
+ pixels_updated |= tile_data.flags.dirty;
+ break;
+ }
+ }
+ }
+
+ node_data.flags.dirty |= pixels_updated;
+}
+
+static void do_mark_dirty_regions(void *__restrict userdata,
+ const int n,
+ const TaskParallelTLS *__restrict UNUSED(tls))
+{
+ TexturePaintingUserData *data = static_cast<TexturePaintingUserData *>(userdata);
+ PBVHNode *node = data->nodes[n];
+ BKE_pbvh_pixels_mark_image_dirty(*node, *data->image_data.image, *data->image_data.image_user);
+}
+
+} // namespace blender::ed::sculpt_paint::paint::image
+
+extern "C" {
+
+using namespace blender::ed::sculpt_paint::paint::image;
+
+bool SCULPT_paint_image_canvas_get(PaintModeSettings *paint_mode_settings,
+ Object *ob,
+ Image **r_image,
+ ImageUser **r_image_user)
+{
+ BLI_assert(r_image);
+ BLI_assert(r_image_user);
+ ImageData image_data;
+ if (!ImageData::init_active_image(ob, &image_data, paint_mode_settings)) {
+ return false;
+ }
+
+ *r_image = image_data.image;
+ *r_image_user = image_data.image_user;
+ return true;
+}
+
+bool SCULPT_use_image_paint_brush(PaintModeSettings *settings, Object *ob)
+{
+ if (!U.experimental.use_sculpt_texture_paint) {
+ return false;
+ }
+ if (ob->type != OB_MESH) {
+ return false;
+ }
+ Image *image;
+ ImageUser *image_user;
+ return BKE_paint_canvas_image_get(settings, ob, &image, &image_user);
+}
+
+void SCULPT_do_paint_brush_image(
+ PaintModeSettings *paint_mode_settings, Sculpt *sd, Object *ob, PBVHNode **nodes, int totnode)
+{
+ Brush *brush = BKE_paint_brush(&sd->paint);
+
+ TexturePaintingUserData data = {nullptr};
+ data.ob = ob;
+ data.brush = brush;
+ data.nodes = nodes;
+
+ if (!ImageData::init_active_image(ob, &data.image_data, paint_mode_settings)) {
+ return;
+ }
+
+ TaskParallelSettings settings;
+ BKE_pbvh_parallel_range_settings(&settings, true, totnode);
+ BLI_task_parallel_range(0, totnode, &data, do_paint_pixels, &settings);
+
+ TaskParallelSettings settings_flush;
+ BKE_pbvh_parallel_range_settings(&settings_flush, false, totnode);
+ BLI_task_parallel_range(0, totnode, &data, do_mark_dirty_regions, &settings_flush);
+}
+}
diff --git a/source/blender/makesdna/DNA_material_types.h b/source/blender/makesdna/DNA_material_types.h
index 38e99896ab1..332317142c7 100644
--- a/source/blender/makesdna/DNA_material_types.h
+++ b/source/blender/makesdna/DNA_material_types.h
@@ -31,6 +31,8 @@ typedef struct TexPaintSlot {
/** Image to be painted on. Mutual exclusive with attribute_name. */
struct Image *ima;
+ struct ImageUser *image_user;
+
/** Custom-data index for uv layer, #MAX_NAME. */
char *uvname;
/**
diff --git a/source/blender/makesdna/DNA_scene_types.h b/source/blender/makesdna/DNA_scene_types.h
index 9cc4d5ed55b..d6c1040110f 100644
--- a/source/blender/makesdna/DNA_scene_types.h
+++ b/source/blender/makesdna/DNA_scene_types.h
@@ -939,6 +939,7 @@ typedef struct PaintModeSettings {
/** Selected image when canvas_source=PAINT_CANVAS_SOURCE_IMAGE. */
Image *canvas_image;
+ ImageUser image_user;
} PaintModeSettings;