@kokoko3k, Thanks! I remember you talking about that on the Discord (and I sent an image of my own experiment).
Here’s what I ended up doing:
vec3 mask(vec3 pixel_value) {
// I removed some overscan stuff here to make it simpler to read...
vec3 mask = vec3(1.0);
float mask_coverage = 1.0;
if (params.SubpixelMaskPattern == 2.0) { // <=1080p
vec3[2] mask_tile = { vec3(1, 0, 1), vec3(0, 1, 0) };
mask = mask_tile[int(mod(floor(viewport_x_coord * params.OutputSize.x), 2.0))];
mask_coverage = 2.0;
} else if (params.SubpixelMaskPattern == 3.0) { // 1080p/1440p
vec3[3] mask_tile = { vec3(0, 0, 1), vec3(0, 1, 0), vec3(1, 0, 0) };
mask = mask_tile[int(mod(floor(viewport_x_coord * params.OutputSize.x), 3.0))];
mask_coverage = 3.0;
} else if (params.SubpixelMaskPattern == 4.0) { // 1440p/4k
vec3[4] mask_tile = { vec3(1, 0, 0), vec3(1, 1, 0), vec3(0, 1, 1), vec3(0, 0, 1) };
mask = mask_tile[int(mod(floor(viewport_x_coord * params.OutputSize.x), 4.0))];
mask_coverage = 2.0;
} else if (params.SubpixelMaskPattern == 5.0) { // 4k, lower TVL
vec3[5] mask_tile = { vec3(1, 0, 0), vec3(1, 0, 1), vec3(0, 0, 1), vec3(0, 1, 0), vec3(0, 1, 0) };
mask = mask_tile[int(mod(floor(viewport_x_coord * params.OutputSize.x), 5.0))];
mask_coverage = 15.0 / 6.0;
}
// For BGR subpixel arrangement, just transpose red and blue.
// Piecewise phase-in. The original image starts phasing in only when we've
// maxed the brightness we can get from our masked image.
float s = mask_coverage / (mask_coverage - 1.0);
vec3 weight = clamp(-s * pixel_value + s, 0.0, 1.0);
return pixel_value * ((1 - weight) + mask_coverage * mask * weight);
}
In practice, I don’t really notice the discontinuity. If the area where the original image starts to phase in is noticeable, I had a backup plan:
// Cubic phase-in. Ramps up the mask to higher strength faster than linear
// but has no discontinuity like piecewise.
float s = mask_coverage / (mask_coverage - 1);
float a = -s + 2.0;
float b = s - 3.0;
vec3 weight = a * pow(pixel_value, 3.0) + b * pow(pixel_value, 2.0) + 1.0;
return pixel_value * ((1 - weight) + mask_coverage * mask * weight);
This looks pretty similar but has no discontinuity. Here’s the graph of the mask strength (on the y-axis) and the pixel value (on the x-axis) for mask_coverage = 3
. Red is the piecewise phase-in and blue is the cubic.
And here’s a gradient with the piecewise phase-in on the top and the cubic on the bottom. It’s 4k and I recommend viewing at native resolution (when it is rescaled you can definitely notice the discontinuity).
But I think your implementation differs or maybe you didn’t aimed for full brightness, as I see that the white shirt retains some of the mask (?).
There are two reasons the white shirt retains some mask. One is that Genesis Plus GX doesn’t actually output full-scale white values. The other is that this is done per-pixel, and the scanline is brightest in the middle, so that’s the part that gets blown out first. The mask is retained at the edges. I was using a max spot width of 0.9, so the scanlines don’t completely touch. That does reduce the brightness slightly, but it’s the scanlines reducing the brightness, not the mask. At a max spot width of 1.0, they’d merge if the values on those lines were full-scale white and the mask would not be visible. Hopefully that makes sense.