Soft, transparent paint stroke textures not blending like I expect

I’m trying to implement soft brushes into my GL 4.5 application. I’ve successfully achieved 100% hard brushes but the soft brush functionality is sub par. This type of paint mode I’m trying to achieve is referred to as “wash” painting in Krita and is the normal mode in GIMP.

For example: in photoshop I have a round brush with low hardness and brush opacity at 10% and hold down the mouse and drag the cursor around I get a uniform stroke with the texture building up to the 10% opacity except around the edges where it fades out. If I click again it’ll build up towards say white at 10% more because of additive blending (?).

In my application with a soft brush texture I always get banding on mouse move because of the fading opacity of the texture. I’ve tried various blending modes and clamping on the opacity after blending the current stroke and the BG texture and they all more or less look like the following :


//pPaintStroke_washF

#version 450 core
#extension GL_ARB_bindless_texture : require

in Vert
{
    vec2 uv;
} v;

layout(bindless_sampler, location = 0) uniform sampler2D blended_64; // blended bg + stroke
layout(location = 0) out vec4 Ci;

uniform vec4 brushRGBA = vec4(1.f, 1.f, 1.f, .001f);

void main()
{
    vec4 blended = texture(blended_64, v.uv);

    if (blended.a == 0.f)
        Ci = vec4(0.f);

    else
    {
		Ci.rgb = brushRGBA.rgb;
		Ci.a = clamp(blended.a, 0.f, brushRGBA.a);
		// Ci.a = clamp(blended.a, blended.a, brushRGBA.a);
    }
}



glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

for (int i = 0; i < Bresenham.size(); ++i)
{
	/*
		//bind brushTempN.fbo1
		//render Bresenham[i] into it
	*/

	glBindFramebuffer(GL_FRAMEBUFFER, brushTempN.fbo1);
	glViewport(0, 0, brushTempN.width, brushTempN.height);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glClearColor(0.f, 0.f, 0.f, 0.f);

	myWin.myGLWidgetSh->glUseProgram2("pPaintStroke_unwashed");

	myWin.paintStroke->s->val_3 = glm::vec3((float)Bresenham[i].size / 100);
	myWin.paintStroke->t->val_3 = glm::vec3(Bresenham[i].coord, 0.f);
	myWin.paintStroke->mvpGet(myWin.allGL[GLidx]);
	myWin.paintStroke->render(myWin.allGL[GLidx]);

	/*
		//bind brushTempN.fbo2
		//render bg - starts as 0 (brushBGN.tex1)
		//render curr stroke (brushTempN.tex1)
		//this blends them
	*/

	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	glBindFramebuffer(GL_FRAMEBUFFER, brushTempN.fbo2);
	glViewport(0, 0, brushTempN.width, brushTempN.height);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glClearColor(0.f, 0.f, 0.f, 0.f);

	myWin.myGLWidgetSh->glUseProgram2("pPaintStroke_washBlend");

	washBlendPhase = 0; myWin.myFSQ->render(myWin.allGL[GLidx]); //render BG
	
	washBlendPhase = 1; myWin.myFSQ->render(myWin.allGL[GLidx]); //render the curr stroke (brushTempN.tex1)

	/*
		//bind brushN.fbo1
		//read brushTempN.tex2_64
		//clamp the blended alpha with Ci.a = clamp(in.a, 0.f, brushRGBA.a)
	*/

	glBindFramebuffer(GL_FRAMEBUFFER, brushN.fbo1);
	glViewport(0, 0, brushN.width, brushN.height);

	myWin.myGLWidgetSh->glUseProgram2("pPaintStroke_wash");
	myWin.myFSQ->render(myWin.allGL[GLidx]);

	/*
		//bind brushBGN.fbo1
		//copy the above brushN.tex1 into it
	*/

	glBindFramebuffer(GL_FRAMEBUFFER, brushBGN.fbo1);
	glViewport(0, 0, brushBGN.width, brushBGN.height);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glClearColor(0.f, 0.f, 0.f, 0.f);

	myWin.myGLWidgetSh->glUseProgram2("pPaintStroke_washBlend");

	washBlendPhase = 2; myWin.myFSQ->render(myWin.allGL[GLidx]); //render new BG
}