Paint like color mixing (Kubelka-Munk)

No, the problem is caused by the spectral data of spectral.js, there is no problem with the way the smudge brush works.

This is the version I built using the branch I submitted for spectral blending, and it doesn’t have the effect of shifting colours even at minimum spacing .

Ok I will further refrain from this discussion as we will never agree and we will circle back in the same fruitless discussions we had before.
I stand by my point that it’s not the spectral data on it’s own, if spectral.js was something new and unused I would question myself but it is already used a lot by others without any problems.

This is the mix from white to black using the previous color as input for the next mix step and it isn’t showing a hint of green:

This is for the devs to resolve and make a standpoint.

Nothing personal @urzeye we just disagree :wink:

I think since there are two implementation for the same feature there will be certain points for disagreement. we can leave the discussion for a few days let the bugs be solved, let people test things and then the devs can take a look at the MR.

So I request everyone to be on topic and discuss in good faith, let us keep the discussion healthy and collaborate to make this feature

8 Likes

I couldn’t agree more

2 Likes
#include <iostream>
#include <vector>
#include <cmath>

static const int SIZE = 36;
static const float EPSILON = 0.00000001;

//Generated red, green and blue spectral data
//For reference but not used
//http://scottburns.us/fast-rgb-to-spectrum-conversion-for-reflectances/
static const float SPD_R[SIZE] = {0.02159246, 0.02029311, 0.02180791, 0.0238033, 0.02520813, 0.02541496, 0.02462128, 0.02097371, 0.0157528, 0.01116804, 0.00857828, 0.00658188, 0.00517172, 0.00454521, 0.00414512, 0.00434311, 0.00523816, 0.00725194, 0.01254366, 0.02806713, 0.09134228, 0.48408109, 0.87037832, 0.93951313, 0.96092699, 0.96862376, 0.97126388, 0.97228582, 0.97189874, 0.97269186, 0.97173481, 0.97234454, 0.97150339, 0.970858, 0.97055387, 0.9696714};
static const float SPD_G[SIZE] = {0.01054241, 0.01087898, 0.01106351, 0.01073657, 0.01168181, 0.01243472, 0.01498691, 0.02010039, 0.03035626, 0.06338896, 0.17342384, 0.56832114, 0.827792, 0.91656047, 0.95200284, 0.96409645, 0.97059086, 0.97250254, 0.9691482, 0.95534465, 0.89263723, 0.5003641, 0.11623672, 0.04795139, 0.02787353, 0.02005796, 0.01738217, 0.01542911, 0.01543808, 0.01454683, 0.01519777, 0.0142859, 0.01506912, 0.01550626, 0.0155458, 0.01630284};
static const float SPD_B[SIZE] = {0.96786514, 0.96882791, 0.96712858, 0.96546014, 0.96311006, 0.96215032, 0.96039181, 0.9589259, 0.95389094, 0.925443, 0.81799789, 0.42509696, 0.16703627, 0.07889433, 0.04385204, 0.03156044, 0.02417098, 0.02024552, 0.01830814, 0.01658822, 0.01602049, 0.01555481, 0.01338496, 0.01253549, 0.01119948, 0.01131827, 0.01135395, 0.01228507, 0.01266319, 0.01276133, 0.01306743, 0.01336957, 0.01342749, 0.01363574, 0.0138936, 0.01402576};

//CIE (1964-10 degree) color matching functions multiplied by the CIE D65 standard illuminant
//normalized by dividing the values by the sum of CIE_CMF_Y * D65
//these are called X-, Y- and Z-bar.
static const float X_BAR[SIZE] = {0.00006469, 0.00021942, 0.0011206, 0.00376671, 0.01188085, 0.02328702, 0.03456028, 0.03722472, 0.03241918, 0.02123373, 0.01049125, 0.00329592, 0.00050705, 0.0009487, 0.00627387, 0.01686504, 0.02869036, 0.04267588, 0.05625615, 0.06947213, 0.08305522, 0.08612824, 0.09046839, 0.08500598, 0.07090844, 0.05063015, 0.03547485, 0.02146875, 0.01251677, 0.00680475, 0.00346465, 0.00149765, 0.00076972, 0.00040738, 0.00016901, 0.00009523};
static const float Y_BAR[SIZE] = {0.00000184, 0.00000621, 0.00003101, 0.00010475, 0.00035365, 0.0009515, 0.00228232, 0.00420743, 0.00668897, 0.00988864, 0.01524983, 0.02141884, 0.03342376, 0.05131129, 0.07040384, 0.0878409, 0.0942514, 0.09795911, 0.09415453, 0.08678319, 0.0788585, 0.06352829, 0.05374276, 0.04264713, 0.03161814, 0.02088573, 0.01386046, 0.00810284, 0.00463022, 0.00249144, 0.00125933, 0.00054166, 0.00027796, 0.00014711, 0.00006103, 0.00003439};
static const float Z_BAR[SIZE] = {0.00030502, 0.00103683, 0.00531327, 0.01795484, 0.057079, 0.11365445, 0.17336305, 0.19621147, 0.18608701, 0.13995396, 0.08917675, 0.04789741, 0.02814633, 0.01613806, 0.0077593, 0.00429626, 0.00200556, 0.00086149, 0.00036905, 0.00019143, 0.00014956, 0.00009231, 0.00006814, 0.00002883, 0.00001577, 0.00000394, 0.00000158, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

//XYZ to RGB matrix, rounded on 8 decimals
//https://github.com/w3c/csswg-drafts/issues/5922
static const float XYZ_RGB[3][3] = {
    {3.24096994, -1.53738318, -0.49861076},
    {-0.96924364, 1.8759675, 0.04155506},
    {0.05563008, -0.20397696, 1.05697151}
};

void reflectanceToXYZ(float *R, float* X, float* Y, float* Z) {
    for (int i = 0; i < SIZE; i++) {
      *X += R[i] * X_BAR[i];
      *Y += R[i] * Y_BAR[i];
      *Z += R[i] * Z_BAR[i];
    }
}

void XYZToLinear(float X, float Y, float Z, float* r, float* g, float* b) {
    *r = X * XYZ_RGB[0][0] + Y * XYZ_RGB[0][1] + Z * XYZ_RGB[0][2];
    *g = X * XYZ_RGB[1][0] + Y * XYZ_RGB[1][1] + Z * XYZ_RGB[1][2];
    *b = X * XYZ_RGB[2][0] + Y * XYZ_RGB[2][1] + Z * XYZ_RGB[2][2];
}

//function for creating a SPD out of the red, green and blue SPD's
void linearToReflectance(float r, float g, float b, float* R) {
    float weightW = fmin(r, fmin(g, b));
    
    float weightR = r - weightW;
    float weightG = g - weightW;
    float weightB = b - weightW;

    for (int i = 0; i < SIZE; i++) {
      R[i] = fmax(EPSILON, weightW + weightR * SPD_R[i] + weightG * SPD_G[i] + weightB * SPD_B[i]);
    }
}

//luminance is the sum of multiplying the reflectance with the Y-bar
float reflectanceToLuminance(float* R) {
    float l = 0.0;

    for (int i = 0; i < SIZE; i++) {
      l += R[i] * Y_BAR[i];
    }

    return l;
}

//formula for calculating the mix factor based on the luminance
//this is needed for generating a perceptually even spread between 0 and 1
//where 0 is color1 and 1 is color2 
float luminanceToConcentration(float l1, float l2, float t) {
    float t1 = l1 * pow(1.0 - t, 2);
    float t2 = l2 * pow(t, 2);

    return t2 / (t1 + t2);
}

void spectralMix(float srcR, float srcG, float srcB, float factor, float* dstR, float* dstG, float* dstB) {
    float R1[36], R2[36], R[36];
    
    linearToReflectance(srcR, srcG, srcB, R1);
    linearToReflectance(*dstR, *dstG, *dstB, R2);

    float l1 = reflectanceToLuminance(R1);
    float l2 = reflectanceToLuminance(R2);

    float c = luminanceToConcentration(l1, l2, factor);
    
    for (int i = 0; i < SIZE; i++) {
        float KS = 0.0;

        KS += (pow(1.0 - R1[i], 2) / (2 * R1[i])) * (1.0 - c);
        KS += (pow(1.0 - R2[i], 2) / (2 * R2[i])) * c;

        float KM = 1.0 + KS - sqrt(pow(KS, 2) + 2 * KS);

        R[i] = KM;
    }

    float X = 0.0, Y = 0.0, Z = 0.0;

    reflectanceToXYZ(R, &X, &Y, &Z);

    XYZToLinear(X, Y, Z, dstR, dstG, dstB);
}


/***************************************************************************************/
void smudge(int num, float *r, float *g, float *b) {
    for (int i = 0; i < num; i++) {
        spectralMix(*r, *g, *b, 0.5, r, g, b);
    }
}

void smudgeError(float sr, float sg, float sb, float *dr, float *dg, float *db) {
    *dr = fabs(*dr - sr);
    *dg = fabs(*dg - sg);
    *db = fabs(*db - sb);
}

int main()
{
    float sr, sg, sb, dr, dg, db;

    sr = dr = 0.5;
    sg = dg = 0.5;
    sb = db = 0.5;

    std::cout<< "target: " << sr << " " << sg << " " << sb << std::endl;

    //Number of mixing times
    int num = 100;

    smudge(num, &dr, &dg, &db);
    std::cout<< "mixed: " << dr << " " << dg << " " << db << std::endl;

    smudgeError(sr, sg, sb, &dr, &dg, &db);
    std::cout<< "error: " << dr << " " << dg << " " << db << std::endl;

    return 0;
}

Hi, @rvanwijnen, this is the test code where the spectral mixing code is all from your MR, you can run it here to see how it runs.

This is the result of mixing the same colour 100 times, which is a small distance for brushes with very little brush spacing:

target: 0.5 0.5 0.5
mixed: 0.468733 0.514685 0.453898
error: 0.0312669 0.0146854 0.0461021

I actually don’t really care whose MR’s will be merged, I don’t use spectral blending related features, I originally submitted the MR about spectral blending to thank a friend of mine for helping me, what I wanted to do has been done, I’m sorry if there is something about my semantics that makes people uncomfortable, probably the reason for that is that the translator I’m using doesn’t convey exactly what I want to say.

3 Likes

Hi, @rvanwijnen and @urzeye!

I added a colorsmudge-brush fix to both the branches, so the colorsmudge brush enables spectral blendmode only when “Spectral” blendmode is selected.

I’ve also built packages for both variants:

@rvanwijnen: krita-5.3.0-spectral-colorsmudge-mr1997-dk2.zip — Яндекс Диск
@urzeye: krita-5.3.0-spectral-colorsmudge-mr1783-dk1.zip — Яндекс Диск

From the brief look it seems like the version from @urzeye generates clearer colors, though it is still very unstable in high smearing mode:

The color falls down to dirty-dark-black very quickly when mixing the same area for some time. I have a feeling like it might be related to the highly nonlinear concentration function, though I’m not sure.

I guess we should just make simple tests (like @urzeye did) for both modes and try to optimize them to satisfy the properties required:

  1. Identity property: A + A -> A
  2. Stability: B = B0; B = mix(dst: B, src: A, opacity: 0.01) | repeat; lim(B) -> A

We could probably add one mode test:

  1. Identity stability: A=A0; A = mix(dst: A, src: A + random_epsilon; opacity: 0.01)|repeat; lib(A)->A0

That is, small fluctuations of the source color should not cause drastic changes in the destination color.

Another possible reason for the issue might be the implementation of the COMPOSITE_COPY_SPECTRAL. I’m not really sure it is correct. The standard implementation should premultiply the colors before blending, but the one in your implementations don’t do that. That might also cause the issue with too quick saturation of the color.

5 Likes

Hi, @dkazakov, since the last time I used XSIMD for optimisation, I used the data provided by @jnyenhuis directly in MR, and from the results of the test it seems that there is a problem with the way he calculates the data, so it caused the colours to darken quickly, the other data I used myself that was provided in the MR discussion didn’t have any colour shifts at all.

    template<class T>
    inline T blend(T src, T srcAlpha, T dst, T dstAlpha, T cfValue) {
        return mul(inv(srcAlpha), dstAlpha, dst) + mul(inv(dstAlpha), srcAlpha, src) + mul(dstAlpha, srcAlpha, cfValue);
    }

Regarding the spectral copy it’s really just a simplified version of this function when cfValue is equal to src. Because for OVER & COPY, when srcAlpha = 1, the mixing result is src, so it can be used whenever srcAlpha = 1, which is how it is calculated in the standard OVER & COPY. In spectral copy, the coefficients of src and dst are just calculated in advance, so your fix is incorrect.

Hm… I will check later. We don’t actually use this function in copy, we have a separate KoCompositeOpCopy2 for that. And my fix tried to make it consistent with that

Hey @urzeye , you are absolutely right!

But this is expected behaviour and has nothing to do with the spectral data.
The problem we’re facing is float precission.

Spectral.js is created for 2 colors as input and 1 color output, the input gets converted to spectral space in which the calculations occur.
The resulting color is also in spectral space and gets converted to XYZ space, from XYZ it gets converted to linear space.

Normally the resulting color will then be converted to sRGB space, gamma is applied and the linear value is multiplied with 255 and gets rounded to a whole number.

This part works as expected and there are be no precission errors.

The smudge engine stays in linear space, there is no rounding and gamma applied, ofcourse this will derail after a lot of itterations (float * float * float, etc…).

To mitigate this we could round the XYZ values to three decimal values before converting from XYZ to linear.

After 1,000,000 runs this is the deviation when rounding is applied:

target: 0.5 0.5 0.5
mixed: 0.499525 0.500199 0.499428
error: 0.000475198 0.00019896 0.000571668 in 1000000

This is acceptable and will get rounded anyway when converted to sRGB.

You can test this yourself:

#include <iostream>
#include <vector>
#include <cmath>

static const int SIZE = 36;
static const float EPSILON = 0.00000001;

//Generated red, green and blue spectral data
//For reference but not used
//http://scottburns.us/fast-rgb-to-spectrum-conversion-for-reflectances/
static const float SPD_R[SIZE] = {0.02159246, 0.02029311, 0.02180791, 0.0238033, 0.02520813, 0.02541496, 0.02462128, 0.02097371, 0.0157528, 0.01116804, 0.00857828, 0.00658188, 0.00517172, 0.00454521, 0.00414512, 0.00434311, 0.00523816, 0.00725194, 0.01254366, 0.02806713, 0.09134228, 0.48408109, 0.87037832, 0.93951313, 0.96092699, 0.96862376, 0.97126388, 0.97228582, 0.97189874, 0.97269186, 0.97173481, 0.97234454, 0.97150339, 0.970858, 0.97055387, 0.9696714};
static const float SPD_G[SIZE] = {0.01054241, 0.01087898, 0.01106351, 0.01073657, 0.01168181, 0.01243472, 0.01498691, 0.02010039, 0.03035626, 0.06338896, 0.17342384, 0.56832114, 0.827792, 0.91656047, 0.95200284, 0.96409645, 0.97059086, 0.97250254, 0.9691482, 0.95534465, 0.89263723, 0.5003641, 0.11623672, 0.04795139, 0.02787353, 0.02005796, 0.01738217, 0.01542911, 0.01543808, 0.01454683, 0.01519777, 0.0142859, 0.01506912, 0.01550626, 0.0155458, 0.01630284};
static const float SPD_B[SIZE] = {0.96786514, 0.96882791, 0.96712858, 0.96546014, 0.96311006, 0.96215032, 0.96039181, 0.9589259, 0.95389094, 0.925443, 0.81799789, 0.42509696, 0.16703627, 0.07889433, 0.04385204, 0.03156044, 0.02417098, 0.02024552, 0.01830814, 0.01658822, 0.01602049, 0.01555481, 0.01338496, 0.01253549, 0.01119948, 0.01131827, 0.01135395, 0.01228507, 0.01266319, 0.01276133, 0.01306743, 0.01336957, 0.01342749, 0.01363574, 0.0138936, 0.01402576};

//CIE (1964-10 degree) color matching functions multiplied by the CIE D65 standard illuminant
//normalized by dividing the values by the sum of CIE_CMF_Y * D65
//these are called X-, Y- and Z-bar.
static const float X_BAR[SIZE] = {0.00006469, 0.00021942, 0.0011206, 0.00376671, 0.01188085, 0.02328702, 0.03456028, 0.03722472, 0.03241918, 0.02123373, 0.01049125, 0.00329592, 0.00050705, 0.0009487, 0.00627387, 0.01686504, 0.02869036, 0.04267588, 0.05625615, 0.06947213, 0.08305522, 0.08612824, 0.09046839, 0.08500598, 0.07090844, 0.05063015, 0.03547485, 0.02146875, 0.01251677, 0.00680475, 0.00346465, 0.00149765, 0.00076972, 0.00040738, 0.00016901, 0.00009523};
static const float Y_BAR[SIZE] = {0.00000184, 0.00000621, 0.00003101, 0.00010475, 0.00035365, 0.0009515, 0.00228232, 0.00420743, 0.00668897, 0.00988864, 0.01524983, 0.02141884, 0.03342376, 0.05131129, 0.07040384, 0.0878409, 0.0942514, 0.09795911, 0.09415453, 0.08678319, 0.0788585, 0.06352829, 0.05374276, 0.04264713, 0.03161814, 0.02088573, 0.01386046, 0.00810284, 0.00463022, 0.00249144, 0.00125933, 0.00054166, 0.00027796, 0.00014711, 0.00006103, 0.00003439};
static const float Z_BAR[SIZE] = {0.00030502, 0.00103683, 0.00531327, 0.01795484, 0.057079, 0.11365445, 0.17336305, 0.19621147, 0.18608701, 0.13995396, 0.08917675, 0.04789741, 0.02814633, 0.01613806, 0.0077593, 0.00429626, 0.00200556, 0.00086149, 0.00036905, 0.00019143, 0.00014956, 0.00009231, 0.00006814, 0.00002883, 0.00001577, 0.00000394, 0.00000158, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};

//XYZ to RGB matrix, rounded on 8 decimals
//https://github.com/w3c/csswg-drafts/issues/5922
static const float XYZ_RGB[3][3] = {
    {3.24096994, -1.53738318, -0.49861076},
    {-0.96924364, 1.8759675, 0.04155506},
    {0.05563008, -0.20397696, 1.05697151}
};

void reflectanceToXYZ(float *R, float* X, float* Y, float* Z) {
    for (int i = 0; i < SIZE; i++) {
      *X += R[i] * X_BAR[i];
      *Y += R[i] * Y_BAR[i];
      *Z += R[i] * Z_BAR[i];
    }
}

void XYZToLinear(float X, float Y, float Z, float* r, float* g, float* b) {
    *r = X * XYZ_RGB[0][0] + Y * XYZ_RGB[0][1] + Z * XYZ_RGB[0][2];
    *g = X * XYZ_RGB[1][0] + Y * XYZ_RGB[1][1] + Z * XYZ_RGB[1][2];
    *b = X * XYZ_RGB[2][0] + Y * XYZ_RGB[2][1] + Z * XYZ_RGB[2][2];
}

//function for creating a SPD out of the red, green and blue SPD's
void linearToReflectance(float r, float g, float b, float* R) {
    float weightW = fmin(r, fmin(g, b));
    
    float weightR = r - weightW;
    float weightG = g - weightW;
    float weightB = b - weightW;

    for (int i = 0; i < SIZE; i++) {
      R[i] = fmax(EPSILON, weightW + weightR * SPD_R[i] + weightG * SPD_G[i] + weightB * SPD_B[i]);
    }
}

//luminance is the sum of multiplying the reflectance with the Y-bar
float reflectanceToLuminance(float* R) {
    float l = 0.0;

    for (int i = 0; i < SIZE; i++) {
      l += R[i] * Y_BAR[i];
    }

    return l;
}

//formula for calculating the mix factor based on the luminance
//this is needed for generating a perceptually even spread between 0 and 1
//where 0 is color1 and 1 is color2 
float luminanceToConcentration(float l1, float l2, float t) {
    float t1 = l1 * pow(1.0 - t, 2);
    float t2 = l2 * pow(t, 2);

    return t2 / (t1 + t2);
}

void spectralMix(float srcR, float srcG, float srcB, float factor, float* dstR, float* dstG, float* dstB) {
    float R1[36], R2[36], R[36];
    
    linearToReflectance(srcR, srcG, srcB, R1);
    linearToReflectance(*dstR, *dstG, *dstB, R2);

    float l1 = reflectanceToLuminance(R1);
    float l2 = reflectanceToLuminance(R2);

    float c = luminanceToConcentration(l1, l2, factor);
    
    for (int i = 0; i < SIZE; i++) {
        float KS = 0.0;

        KS += (pow(1.0 - R1[i], 2) / (2 * R1[i])) * (1.0 - c);
        KS += (pow(1.0 - R2[i], 2) / (2 * R2[i])) * c;

        float KM = 1.0 + KS - sqrt(pow(KS, 2) + 2 * KS);

        R[i] = KM;
    }

    float X = 0.0, Y = 0.0, Z = 0.0;

    reflectanceToXYZ(R, &X, &Y, &Z);

    // XYZToLinear(X, Y, Z, dstR, dstG, dstB);
    XYZToLinear(roundf(X * 1000) / 1000, roundf(Y * 1000) / 1000, roundf(Z * 1000) / 1000, dstR, dstG, dstB);
}

/***************************************************************************************/

void smudge(int num, float *r, float *g, float *b) {
    for (int i = 0; i < num; i++) {
        spectralMix(*r, *g, *b, 0.5, r, g, b);
    }
}

void smudgeError(float sr, float sg, float sb, float *dr, float *dg, float *db) {
    *dr = fabs(*dr - sr);
    *dg = fabs(*dg - sg);
    *db = fabs(*db - sb);
}

int main()
{
    float sr, sg, sb, dr, dg, db;

    sr = dr = 0.5;
    sg = dg = 0.5;
    sb = db = 0.5;

    std::cout<< "target: " << sr << " " << sg << " " << sb << std::endl;

    //Number of mixing times
    int num = 1000000;

    smudge(num, &dr, &dg, &db);
    std::cout<< "mixed: " << dr << " " << dg << " " << db << std::endl;

    smudgeError(sr, sg, sb, &dr, &dg, &db);
    std::cout<< "error: " << dr << " " << dg << " " << db << " in " << num << std::endl;

    return 0;
}

Again this has nothing to do with the spectral data.

Thank you for making clear what the problem is, I have a lot more understanding from which you’re coming from.

@dkazakov I have updated the merge request, can you build a new version from this?

4 Likes

No, spectral copy was originally identical to standard copy, your fix changed it, they are both actually simplified versions of the blend function above.

When the opacity is locked, dstApha is counted as 1. You can combine this with alphaLocked in the standard blend mode, I just simplified the code.

I understand, it’s really nice to be able to fix this, I actually don’t really want to spend more time in this MR, I think the best way to do it would be to add the option for the artist to choose how they like to mix the results, that shouldn’t be difficult but I shouldn’t be able to do it after that.

Hi, @urzeye!

The problem is that the current implementations on each of the method are not yet ready for the use by real users, since they generate weird colors in some cases. What I’m trying to do now is to find out the reasons for that and to resolve all the issues that block the feature from the normal use.

1 Like

Please look closely on this line :slight_smile: The highlighted branch is only taken when alpha is locked, which is not a case while normal painting. This line is actually our implementation of “Over” blendmode, not copy :slight_smile:

UPD:
Btw, I tried the old formula (over-like) and it seem like it created dimmed blending… But, otherwise, there is no significant difference.

Hi, all!

Here are the updated packages:

For @urzeye version: krita-5.3.0-spectral-colorsmudge-mr1783-dk2.zip — Яндекс Диск
For @rvanwijnen version: krita-5.3.0-spectral-colorsmudge-mr1997-dk3.zip — Яндекс Диск

The @urzeye version seems to create clearer colors, but it is still very easy to get gray/dirty result when smudging at a very low opacity level. It seems like stability property is not fulfilled, but I’m not very sure.

The new version with @rvanwijnen is much better, but it still jumps into black very easily.

4 Likes

I like it, where does it jump to black?
I don’t see any ‘dirty’ colors to be honest and sometimes paint just looks dirty.

2 Likes

Some observations of the latest version (98258dfb):
With ‘Spectral’ blending mode:
If ‘Use new smudge algorithm’ is not checked the brush preview turns green. Using a white color on a white background (the scratchpad for instance) turns yellow.
If ‘Use new smudge algorithm’ is checked and the Color Rate is not 100%, the brush preview turns green. White-on-white turns yellow/green.
These don’t happen with ‘Copy Spectral’.

Another observation- using Copy Spectral with a non-Smudge Engine brush erases the contents of the tile underneath it. (At first it appears to only erase surrounding pixels, but refreshing the layer visibilty shows the true extent.) This doesn’t happen with Spectral.

3 Likes

I have a better understanding of brush blending now, up until now I focused on the layer blending.

I think the Spectral brush blending works but Copy Spectral is wrong, is Copy Spectral really needed?

I have noticed some blended colors produce a third color that has a much darker greyscale value. I would expect the values of blended colors to be softer.

Here is an example with colors that are the same “grey”:
first is a normal blend
second is a spectral blend
third is a mockup of what I would expect the blend to do

Is it the right behaviour?
Someone that has a software with real color mixing can confirm it happens there too?

Some other color combos result in a very dark color: