I'm attempting to create a shader that additively blends colored "blobs" (kind of like particles) on top of one another. This seems like it should be a straightforward task but I'm getting strange "banding"-like artifacts when the blobs blend.
First off, here's the behavior I'm after (replicated using Photoshop layers):
Note that the three color layers are all set to blendmode "Linear Dodge (Add)" which as far as I understand is Photoshop's "additive" blend mode.
If I merge the color layers and leave the resulting layer set to "Normal" blending, I'm then free to change the background color as I please.
Obviously additive blending will not work on top of a non-black background, so in the end I will also want/need the shader to support this pre-merging of colors before finally blending into a background that could have any color. However, I'm content for now to only focus on getting the additive-on-top-of-black blending working correctly, because it's not.
Here's my shader code in its current state.
const int MAX_SHAPES = 10;
vec2 spread = vec2(0.3, 0.3);
vec2 offset = vec2(0.0, 0.0);
float shapeSize = 0.3;
const float s = 1.0;
float shapeColors[MAX_SHAPES * 3] = float[MAX_SHAPES * 3] (
s, 0.0, 0.0,
0.0, s, 0.0,
0.0, 0.0, s,
s, 0.0, 0.0,
s, 0.0, 0.0,
s, 0.0, 0.0,
s, 0.0, 0.0,
s, 0.0, 0.0,
s, 0.0, 0.0,
s, 0.0, 0.0
);
vec2 motionFunction (float i) {
float t = iTime;
return vec2(
(cos(t * 0.31 + i * 3.0) + cos(t * 0.11 + i * 14.0) + cos(t * 0.78 + i * 30.0) + cos(t * 0.55 + i * 10.0)) / 4.0,
(cos(t * 0.13 + i * 33.0) + cos(t * 0.66 + i * 38.0) + cos(t * 0.42 + i * 83.0) + cos(t * 0.9 + i * 29.0)) / 4.0
);
}
float blend (float src, float dst, float alpha) {
return alpha * src + (1.0 - alpha) * dst;
}
void mainImage (out vec4 fragColor, in vec2 fragCoord) {
float aspect = iResolution.x / iResolution.y;
float x = (fragCoord.x / iResolution.x) - 0.5;
float y = (fragCoord.y / iResolution.y) - 0.5;
vec2 pixel = vec2(x, y / aspect);
vec4 totalColor = vec4(0.0, 0.0, 0.0, 0.0);
for (int i = 0; i < MAX_SHAPES; i++) {
if (i >= 3) {
break;
}
vec2 shapeCenter = motionFunction(float(i));
shapeCenter *= spread;
shapeCenter += offset;
float dx = shapeCenter.x - pixel.x;
float dy = shapeCenter.y - pixel.y;
float d = sqrt(dx * dx + dy * dy);
float ratio = d / shapeSize;
float intensity = 1.0 - clamp(ratio, 0.0, 1.0);
totalColor.x = totalColor.x + shapeColors[i * 3 + 0] * intensity;
totalColor.y = totalColor.y + shapeColors[i * 3 + 1] * intensity;
totalColor.z = totalColor.z + shapeColors[i * 3 + 2] * intensity;
totalColor.w = totalColor.w + intensity;
}
float alpha = clamp(totalColor.w, 0.0, 1.0);
float background = 0.0;
fragColor = vec4(
blend(totalColor.x, background, alpha),
blend(totalColor.y, background, alpha),
blend(totalColor.z, background, alpha),
1.0
);
}
And here's a ShaderToy version where you can view it live — https://www.shadertoy.com/view/wlf3RM Or as a video — https://streamable.com/un25t
The visual artifacts should be pretty obvious, but here's a video that points them out: https://streamable.com/kxaps (I think they are way more prevalent in the video linked before this one, though. The motion really make them pop out.)
Also as a static image for comparison:
Basically, there are "edges" that appear on certain magical thresholds. I have no idea how they got there or how to get rid of them. Your help would be highly appreciated.
The inside lines are where totalColor.w
reaches 1 and so alpha
is clamped to 1 inside them. The outside ones that you've traced in white are the edges of the circles.
I modified your ShaderToy link by changing float alpha = clamp(totalColor.w, 0.0, 1.0);
to float alpha = 1.0;
and float intensity = 1.0 - clamp(ratio, 0.0, 1.0);
to float intensity = smoothstep(1.0, 0.0, ratio);
(to smooth out the edges of the circles) and now it looks like the first picture.