Fix #143474: Color Space inconsistent between 2D and 3D texture paint

The short description of the root of the problem is that 2D texture
paint properly handled non-color paint while 3D painting did not (for
both painting and color sampling).

This change makes it so 3D texture paint avoids color space conversion
of brush color (which is assumed to be in display space) to scene linear
when painting on images set to Non-Color.

On the design level this change moves code closer to the following
design:
- Brush color is specified in display space.
- Float buffers are in scene linear space (or a non-color).
- When painting on color float buffers brush color gets linearized
- When painting on non-color float buffers brush color is used as-is.

For painting on non-color images it means: values from brush color
are used as-is, without any color space conversion.

The change makes 3D painting behave the same as 2D painting.

There are a couple of tricky parts remaining. Relatively small thing
is that the color displayed on the brush color swatch is not what it
will be after making a stroke. This is because brush colors are used
as-is, image is not color managed (and is drawn as-is on a surface
which is assumed to be in scene linear), and the brush color swatch
is color managed. Generally it means the swatch color is darker than
what the stroke on non-color image would look like.

The bigger topic is color sampling that read pixel value from frame
buffer. This change makes it so sampling color in such cases (by either
using eyedropper from the color swatch popup, or by using Shift-X
sampling operator outside of painting object/texture) will set brush
color in a way that making stroke will result in the same color as it
was on the screen. When painting on non-color images it means the
brush color will be sampled darker than when painting on a color
textures.

This introduces some context dependency to the way how sampling works
which might be undesirable.

Pull Request: https://projects.blender.org/blender/blender/pulls/143642
This commit is contained in:
Sergey Sharybin
2025-08-08 17:58:44 +02:00
committed by Sergey Sharybin
parent a55e0b9516
commit cc6a6fe893
3 changed files with 123 additions and 5 deletions

View File

@@ -88,6 +88,63 @@ static void eyedropper_draw_cb(const wmWindow * /*window*/, void *arg)
eyedropper_draw_cursor_text_region(eye->cb_win_event_xy, eye->sample_text);
}
/* A heuristic to check whether the current eyedropper destination property is used for non-color
* painting. If so, the eyedropper will ignore the PROP_COLOR_GAMMA nature of the property and
* not convert linear colors to display space.
*
* The current logic is targeting texture painting, both 2D and 3D. It assumes that invoking the
* operator from 3D viewport means 3D painting, and invoking from image editor means 2D painting.
*
* For the 3D painting the function checks whether active object is in texture paint mode, and if
* so checks the active image (via material slot, or the explicitly specified image) to have
* non-color (data) colorspace.
*
* For the 2D painting it checks the active image editor's image colorspace.
*
* Since brush color could be re-used from multiple spaces the check is not fully reliable: it is
* possible to invoke sampling from one editor and do stroke in other editor. There is no easy way
* of dealing with this, and it is unlikely to be a common configuration. */
static bool is_data_destination(const bContext *C, const Eyedropper *eye)
{
if (eye->ptr.type != &RNA_Brush) {
return false;
}
const View3D *v3d = CTX_wm_view3d(C);
if (v3d) {
/*const*/ Object *object = CTX_data_active_object(C);
if (!object) {
return false;
}
if ((object->mode & OB_MODE_TEXTURE_PAINT) == 0) {
return false;
}
const Scene *scene = CTX_data_scene(C);
const ImagePaintSettings &settings = scene->toolsettings->imapaint;
Image *image = nullptr;
if (settings.mode == IMAGEPAINT_MODE_MATERIAL) {
Material *material = BKE_object_material_get(object, object->actcol);
if (material && material->texpaintslot) {
image = material->texpaintslot[material->paint_active_slot].ima;
}
}
else if (settings.mode == IMAGEPAINT_MODE_IMAGE) {
image = settings.canvas;
}
return image && IMB_colormanagement_space_name_is_data(image->colorspace_settings.name);
}
const SpaceImage *space_image = CTX_wm_space_image(C);
if (space_image) {
return space_image->image &&
IMB_colormanagement_space_name_is_data(space_image->image->colorspace_settings.name);
}
return false;
}
static bool eyedropper_init(bContext *C, wmOperator *op)
{
Eyedropper *eye = MEM_new<Eyedropper>(__func__);
@@ -137,7 +194,7 @@ static bool eyedropper_init(bContext *C, wmOperator *op)
eye->draw_handle_sample_text = WM_draw_cb_activate(eye->cb_win, eyedropper_draw_cb, eye);
}
if (prop_subtype != PROP_COLOR) {
if (prop_subtype != PROP_COLOR && !is_data_destination(C, eye)) {
Scene *scene = CTX_data_scene(C);
const char *display_device;

View File

@@ -5749,6 +5749,17 @@ static bool project_paint_op(void *state, const float lastpos[2], const float po
return touch_any;
}
static bool has_data_projection_paint_image(const ProjPaintState &ps)
{
for (int i = 0; i < ps.image_tot; i++) {
const ImBuf *ibuf = ps.projImages[i].ibuf;
if (ibuf->colormanage_flag & IMB_COLORMANAGE_IS_DATA) {
return true;
}
}
return false;
}
static void paint_proj_stroke_ps(const bContext * /*C*/,
void *ps_handle_p,
const float prev_pos[2],
@@ -5782,7 +5793,12 @@ static void paint_proj_stroke_ps(const bContext * /*C*/,
pressure,
nullptr,
ps->paint_color);
srgb_to_linearrgb_v3_v3(ps->paint_color_linear, ps->paint_color);
if (has_data_projection_paint_image(*ps)) {
copy_v3_v3(ps->paint_color_linear, ps->paint_color);
}
else {
srgb_to_linearrgb_v3_v3(ps->paint_color_linear, ps->paint_color);
}
}
else if (ps->brush_type == IMAGE_PAINT_BRUSH_TYPE_MASK) {
ps->stencil_value = brush->weight;

View File

@@ -59,6 +59,8 @@
#include "WM_api.hh"
#include "WM_types.hh"
#include "IMB_colormanagement.hh"
#include "paint_intern.hh"
/* -------------------------------------------------------------------- */
@@ -180,6 +182,25 @@ static void paint_sample_color(
SpaceImage *sima = CTX_wm_space_image(C);
const View3D *v3d = CTX_wm_view3d(C);
bool is_data = false;
if (v3d) {
const ImagePaintSettings *imapaint = &scene->toolsettings->imapaint;
if (imapaint->mode == IMAGEPAINT_MODE_MATERIAL) {
ViewLayer *view_layer = CTX_data_view_layer(C);
Object *object = BKE_view_layer_active_object_get(view_layer);
const Material *material = BKE_object_material_get(object, object->actcol);
if (material && material->texpaintslot) {
const Image *image = material->texpaintslot[material->paint_active_slot].ima;
is_data = image && IMB_colormanagement_space_name_is_data(image->colorspace_settings.name);
}
}
else {
const Image *image = imapaint->canvas;
is_data = image && IMB_colormanagement_space_name_is_data(image->colorspace_settings.name);
}
}
if (v3d && texpaint_proj) {
/* first try getting a color directly from the mesh faces if possible */
ViewLayer *view_layer = CTX_data_view_layer(C);
@@ -257,10 +278,17 @@ static void paint_sample_color(
rgba_f = math::clamp(rgba_f, 0.0f, 1.0f);
straight_to_premul_v4(rgba_f);
if (use_palette) {
linearrgb_to_srgb_v3_v3(color->rgb, rgba_f);
if (ibuf->colormanage_flag & IMB_COLORMANAGE_IS_DATA) {
copy_v3_v3(color->rgb, rgba_f);
}
else {
linearrgb_to_srgb_v3_v3(color->rgb, rgba_f);
}
}
else {
linearrgb_to_srgb_v3_v3(rgba_f, rgba_f);
if ((ibuf->colormanage_flag & IMB_COLORMANAGE_IS_DATA) == 0) {
linearrgb_to_srgb_v3_v3(rgba_f, rgba_f);
}
BKE_brush_color_set(paint, br, rgba_f);
}
}
@@ -291,7 +319,6 @@ static void paint_sample_color(
/* Sample from the active image buffer. The sampled color is in
* Linear Scene Reference Space. */
float rgba_f[3];
bool is_data;
if (ED_space_image_color_sample(sima, region, blender::int2(x, y), rgba_f, &is_data)) {
if (!is_data) {
linearrgb_to_srgb_v3_v3(rgba_f, rgba_f);
@@ -314,6 +341,24 @@ static void paint_sample_color(
CTX_wm_window(C),
blender::int2(x + region->winrct.xmin, y + region->winrct.ymin),
rgb_fl);
/* The sampled color is in display space, which is what it is supposed to be when painting on
* an image with known colorspace. When painting on non-color/data textures convert display to
* scene linear so that painting with the new color will produce the same color after the
* texture comes via rendering/color management. */
if (is_data) {
/* Note that the logic for the image sampling above uses hardcoded linear<->srgb conversion,
* as well does the do_projectpaint_draw(). For the consistency use hardcoded conversion here
* as well.
*
* Ideally it should become something like:
* const ColorManagedDisplay *display = IMB_colormanagement_display_get_named(
* scene->display_settings.display_device);
* IMB_colormanagement_display_to_scene_linear_v3(rgb_fl, display);
*/
srgb_to_linearrgb_v3_v3(rgb_fl, rgb_fl);
}
if (use_palette) {
copy_v3_v3(color->rgb, rgb_fl);
}