Main issue :
I'm trying to create a brush effect using WebGL. The issue arises when strokes intersect—currently, the blending doesn't look natural.
Here’s what I get:
Here’s what I’d like to achieve (Photoshop-like result):
Fragment Shader Code:
precision lowp float;
uniform vec2 uResolution; // screen resolution
uniform vec2 actualMouse; // mouse actual position
uniform vec2 previousMouse; // previous actual position
uniform float mouseIsPressed; // is the position pressed
uniform sampler2D lastFrame; // remember the last frame
varying vec2 vTexCoord; // from vertex shder
/* ******************************** */
/* projection of a point P onto a segment AB
/* ******************************** */
vec2 projectPointOnSegment(vec2 a, vec2 b, vec2 p){
vec2 ab = vec2(b.x - a.x, b.y - a.y);
vec2 ap = vec2(p.x - a.x, p.y - a.y);
float projection = dot(ap,ab);
float abLen = length(ab);
float abLenSq = abLen*abLen;
float d = projection / abLenSq;
vec2 projectionPoint;
if(d <= 0.0){ projectionPoint = a; }
else if(d >= 1.0){ projectionPoint = b; }
else{
projectionPoint = a + ab*d;
}
return projectionPoint;
}
/* ******************************** */
/* returns the distance of the current pixel
/* to the line drawn by the mouse
/* in vec2 format to obtain x and y distances
/* ******************************** */
vec2 projectPoint(vec2 pointA, vec2 pointB, vec2 point){
vec2 projectionPoint;
if(pointA == pointB){
projectionPoint = pointA;
}else{
projectionPoint = projectPointOnSegment(
pointA,
pointB,
point
);
}
return projectionPoint;
}
/* intensity */
float getIntensity(vec2 projectedPoint, vec2 position){
float t;
t = distance( position, projectedPoint );
t = smoothstep(0.,.1,t);
t = clamp(.0,1.,t);
return t;
}
void main() {
vec2 position = gl_FragCoord.xy / uResolution; // Normalisation
vec3 previous = texture2D(lastFrame,vTexCoord).xyz;
vec2 projectedPoint = projectPoint(actualMouse,previousMouse,position);
float intensity;
if(mouseIsPressed == 1.){
intensity = getIntensity(projectedPoint, position);
}else{
intensity = 1.;
}
intensity = min(intensity,previous.x);
vec3 brush = vec3(intensity);
gl_FragColor = vec4(brush, 1.);
}
Attempted Solution (Smooth Minimum Function):
I tried using a smooth minimum function like the ones described in this article by Inigo Quilez, but it results in a blurry effect around the cursor instead of smooth blending.
Here is what I get:
This approach creates the desired effect where the lines intersect (in the center), but it also introduces an unwanted overlapping effect at the junction of two lines (in the corners).
With the following code:
/* smoothMinimum */
float smin( float a, float b, float k ){
k *= 1.0/(1.0-sqrt(0.5));
return max(k,min(a,b)) -
length(max(k-vec2(a,b),0.0));
}
void main() {
vec2 position = gl_FragCoord.xy / uResolution; // Normalisation
vec3 previous = texture2D(lastFrame,vTexCoord).xyz;
vec2 projectedPoint = projectPoint(actualMouse,previousMouse,position);
vec2 projectedPointOnLineA = projectPoint(vec2(.25,.25),vec2(.75,.75),position);
float lineA = getIntensity(projectedPointOnLineA,position);
vec2 projectedPointOnLineB = projectPoint(vec2(.75,.25),vec2(.25,.75),position);
float lineB = getIntensity(projectedPointOnLineB,position);
vec2 projectedPointOnLineC = projectPoint(vec2(.25,.25),vec2(.25,.75),position);
float lineC = getIntensity(projectedPointOnLineC,position);
float intensity;
intensity = smin(lineB,lineA,.02);
intensity = smin(intensity,lineC,.02);
intensity = smoothstep(0.,.05,intensity);
intensity = clamp(.0,1.,intensity);
vec3 brush = vec3(intensity);
gl_FragColor = vec4(brush, 1.);
}
Another attempt
Another attempt was to use the smooth minimum function following the mouse pointer, combined with a post-shader (pre-shader + post-shader), in order to retain the values while displaying something readable. This approach produces an effect closer to what I want at the intersections, but the brush ends up being overloaded.
Here is what I get:
Pre-shader:
/* smoothMinimum */
float smin( float a, float b, float k ){
k *= 1.0/(1.0-sqrt(0.5));
return max(k,min(a,b)) -
length(max(k-vec2(a,b),0.0));
}
void main() {
vec2 position = gl_FragCoord.xy / uResolution; // Normalisation
vec3 previous = texture2D(lastFrame,vTexCoord).xyz;
vec2 projectedPoint = projectPoint(actualMouse,previousMouse,position);
float intensity;
if(mouseIsPressed == 1.){
intensity = getIntensity(projectedPoint, position);
}else{
intensity = 1.;
}
intensity = smin(intensity,previous.x,.02);
vec3 brush = vec3(intensity);
gl_FragColor = vec4(brush, 1.);
}
Post-shader:
uniform vec2 uResolution; // Résolution écran
varying vec2 vTexCoord;
uniform sampler2D fromPreShader;
void main() {
vec2 position = gl_FragCoord.xy / uResolution; // Normalisation
vec3 newColor = texture2D(fromPreShader,vTexCoord).xyz;
float i = newColor.x;
i = smoothstep(0.,.02,i);
gl_FragColor = vec4(vec3(i), 1.);
}
Question:
What would be a better approach to achieving a natural blending effect when strokes overlap? Are there known techniques for handling brush intersection smoothly in WebGL shaders?
I assume this has been solved before, but I couldn't find relevant resources. Any pointers would be greatly appreciated!