fix(style): dither the surface tint so it stops banding
"Horizontal scanlines" names it exactly. An alpha ramp from 0x34 to 0 has 52 distinct values, so across a 600px pane each one owns a stripe about 12 pixels tall, and the eye reads those stripes as scanlines. Raising the contrast only buys more of them, thinner. So it dithers. Four ramps at a quarter alpha each, every one ending slightly lower than the last, so they share a starting colour but run at different slopes. Their step boundaries land on different rows, and where one layer has stepped up its neighbours have not, which puts the blend between two quantised values. Same total tint, four times the effective resolution, three extra draw calls. Layers are staggered by slope rather than offset on purpose: shifting them would leave a gap at the top where fewer layers overlap, which reads as a bright band. That trades one artefact for a worse one. The tint is roughly twice as strong now, which the settings pane wanted anyway, and strength is a parameter so the chat log can stay at 45% of it.
This commit is contained in:
@@ -120,6 +120,69 @@ internal static class DrawListExtensions
|
||||
dl.AddRectFilledMultiColor(min, max, top, top, bottom, bottom);
|
||||
}
|
||||
|
||||
// A vertical tint that does not band.
|
||||
//
|
||||
// The problem is quantisation, not the ramp: an alpha sweep from 26 to 0
|
||||
// only has 26 distinct values, so across 500 pixels each one owns a stripe
|
||||
// roughly 20px tall -- read as horizontal scanlines. Raising the contrast
|
||||
// just gives more, thinner stripes.
|
||||
//
|
||||
// The fix is to dither. Instead of one ramp, several at a fraction of the
|
||||
// alpha each, every one offset by a sub-step of the height. Their stripe
|
||||
// boundaries land at different rows, so where one layer steps up its
|
||||
// neighbours have not yet, and the blend between them lands between two
|
||||
// quantised values. Same total tint, layers times the effective resolution,
|
||||
// for one extra draw call per layer.
|
||||
public static void DrawDitheredVTint(
|
||||
this ImDrawListPtr dl,
|
||||
Vector2 min,
|
||||
Vector2 max,
|
||||
uint topAbgr,
|
||||
uint bottomAbgr,
|
||||
int layers = 4
|
||||
)
|
||||
{
|
||||
if (layers < 1)
|
||||
layers = 1;
|
||||
|
||||
var height = max.Y - min.Y;
|
||||
if (height <= 0f)
|
||||
return;
|
||||
|
||||
// Every layer starts at the top edge and ends slightly lower than the
|
||||
// last, so they share a starting colour but run at different slopes.
|
||||
// Shifting them instead would leave a gap at the top where fewer layers
|
||||
// overlap, which reads as a bright band -- trading one artefact for a
|
||||
// worse one.
|
||||
//
|
||||
// The stagger is a fraction of one quantisation band, so the steps
|
||||
// interleave rather than landing on top of each other.
|
||||
var steps = MathF.Max(1f, ((topAbgr >> 24) & 0xFFu) + ((bottomAbgr >> 24) & 0xFFu));
|
||||
var band = height / steps;
|
||||
var share = 1f / layers;
|
||||
var top = ScaleAlpha(topAbgr, share);
|
||||
var bottom = ScaleAlpha(bottomAbgr, share);
|
||||
|
||||
for (var i = 0; i < layers; i++)
|
||||
{
|
||||
var stretch = band * (i / (float)layers);
|
||||
dl.AddRectFilledMultiColor(
|
||||
min,
|
||||
new Vector2(max.X, max.Y + stretch),
|
||||
top,
|
||||
top,
|
||||
bottom,
|
||||
bottom
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint ScaleAlpha(uint abgr, float factor)
|
||||
{
|
||||
var a = (uint)Math.Clamp(MathF.Round(((abgr >> 24) & 0xFFu) * factor), 0f, 255f);
|
||||
return (abgr & 0x00FFFFFFu) | (a << 24);
|
||||
}
|
||||
|
||||
// A rule that fades out along its length instead of stopping dead. A hard
|
||||
// line boxes content in; a fading one suggests a boundary without drawing a
|
||||
// wall, which is the whole difference between a heading and a header bar.
|
||||
|
||||
@@ -28,7 +28,12 @@ internal sealed class SurfaceBackdrop
|
||||
// Call right after entering a child, before its content. GetWindowDrawList
|
||||
// is then that child's own list, so everything lands behind the content
|
||||
// without a channel split.
|
||||
internal void Draw(float accentWashHeight = 0.38f, float darken = 0f, float moteIntensity = 1f)
|
||||
internal void Draw(
|
||||
float accentWashHeight = 0.38f,
|
||||
float darken = 0f,
|
||||
float moteIntensity = 1f,
|
||||
float strength = 1f
|
||||
)
|
||||
{
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
var min = ImGui.GetWindowPos();
|
||||
@@ -46,9 +51,13 @@ internal sealed class SurfaceBackdrop
|
||||
// visible underneath, the ramp only tints it, and the game showing
|
||||
// through breaks up any step that is left.
|
||||
var opacity = ((ImGui.GetColorU32(ImGuiCol.WindowBg) >> 24) & 0xFFu) / 255f;
|
||||
var lift = 0x00FFFFFFu | ((uint)(0x0E * opacity) << 24);
|
||||
var drop = 0x00000000u | ((uint)(0x1A * opacity) << 24);
|
||||
dl.AddRectFilledMultiColor(min, max, lift, lift, drop, drop);
|
||||
var lift = 0x00FFFFFFu | ((uint)(0x1E * strength * opacity) << 24);
|
||||
var drop = 0x00000000u | ((uint)(0x34 * strength * opacity) << 24);
|
||||
|
||||
// Dithered, because a plain ramp bands: the alpha sweep has only a few
|
||||
// dozen distinct values, so each owns a stripe tens of pixels tall, seen
|
||||
// as horizontal scanlines across the pane.
|
||||
dl.DrawDitheredVTint(min, max, lift, drop);
|
||||
|
||||
if (darken > 0f)
|
||||
dl.AddRectFilled(min, max, (uint)(0xFF * darken * opacity) << 24);
|
||||
|
||||
@@ -389,7 +389,7 @@ internal sealed class MainWindow : Window, IFocusableChatWindow
|
||||
// a settings pane, which is read in glances; a chat log is read
|
||||
// line by line, and anything drifting behind the text competes
|
||||
// with it. What is left is barely a texture.
|
||||
_backdrop.Draw(accentWashHeight: 0f, moteIntensity: 0.10f);
|
||||
_backdrop.Draw(accentWashHeight: 0f, moteIntensity: 0.10f, strength: 0.45f);
|
||||
|
||||
if (_activeTab is not null)
|
||||
_messages.Draw(_activeTab);
|
||||
|
||||
Reference in New Issue
Block a user