Dither3DInclude.cgincFile: shaders/gbuffers_skybasic.fsh:37
Severity: High — visual blowout at any non-default exposure/offset
applyDither3DColor() already applies DITHER_EXPOSURE * color + DITHER_OFFSET internally (line 53 of dither3d_color.glsl). The sky shader was pre-applying the same transform:
// BEFORE (bug):
vec3 skyColor = clamp(glcolor.rgb * DITHER_EXPOSURE + DITHER_OFFSET, 0.0, 1.0);
vec3 result = applyDither3DColor(skyUV, screenPos, dx, dy, skyColor);
// Result: exposure and offset squared (over-exposed sky)
// AFTER (fix):
vec3 result = applyDither3DColor(skyUV, screenPos, dx, dy, glcolor.rgb);
All other gbuffer fragment shaders pass raw colors — only gbuffers_skybasic.fsh had this issue.
File: tests/test_math.py:15-30
Severity: Medium — test was validating a different algorithm than the shader uses
The Python compute_uv_frequency() computed sqrt(sqrt(lambda1 * lambda2)) — a geometric mean of eigenvalue square roots — instead of the SVD singular values the shader actually computes. The shader's algorithm:
float Q = dot(dx,dx) + dot(dy,dy); // Frobenius norm squared
float R = dx.x*dy.y - dx.y*dy.x; // determinant
float disc = sqrt(Q*Q - 4.0*R*R);
vec2 freq = sqrt(vec2(Q + disc, Q - disc) * 0.5); // singular values (sigma_1, sigma_2)
The test now matches this exactly with three validated cases:
Also fixed: Unicode characters (checkmarks, arrows) replaced with ASCII for Windows console compatibility.
File: shaders/lib/dither3d_core.glsl:98
// BEFORE (confusing):
float dotRadius = 0.25 / sqrt(activeDots * 0.25);
// AFTER (clear):
float dotRadius = 0.5 / sqrt(activeDots);
Both expressions are mathematically identical. The new form directly shows the relationship: radius scales as 0.5 / sqrt(N), giving 0.125 for 16 dots and 0.25 for 4 dots.
Files: shaders/lib/dither3d_options.glsl:41, shaders/lib/dither3d_config.glsl:63
#define PALETTE_COLOR_MATCH 1 // was 0
Color-aware palette matching preserves hues by finding the two closest palette colors and dithering between them. This gives better results for colored palettes (CGA, Pico-8, Nord, Eldritch). The GAMEBOY profile explicitly overrides to 0 (luminance-based) for authentic retro look.
Fragment shader
-> dither3d_options.glsl (defines all macros first)
-> dither3d_color.glsl
-> dither3d_core.glsl
-> dither3d_config.glsl (fallback defaults via #ifndef)
-> dither3d_utils.glsl
-> dither3d_palettes.glsl
-> dither3d_utils.glsl (guard prevents double include)
All 6 modules have unique, consistent include guards:
| Module | Guard |
|---|---|
dither3d_options.glsl | DITHER3D_OPTIONS_GLSL |
dither3d_config.glsl | DITHER3D_CONFIG_GLSL |
dither3d_core.glsl | DITHER3D_CORE_GLSL |
dither3d_utils.glsl | DITHER3D_UTILS_GLSL |
dither3d_palettes.glsl | DITHER3D_PALETTES_GLSL |
dither3d_color.glsl | DITHER3D_COLOR_GLSL |
All vertex shaders correctly compute and pass screenPos = gl_Position for radial compensation. World position and normal calculations are consistent across all geometry types:
All fragment shaders follow the same pattern:
dither3d_options.glsl (first, defines macros)dither3d_color.glsl (pulls in everything)applyDither3DColorSimple() or applyDither3DColor()gl_FragData[0] with proper alpha preservationshaders.properties: 14 RENDER_STYLE values, 8 profiles, proper slider ranges, nested submenu structurelang/en_US.lang: all options, values, profiles, and screen names have labelsshaders.properties → dither3d_options.glsl → dither3d_config.glslgetPaletteColor() centralizes all access with clamp() bounds safety| Original (HLSL) | Port (GLSL) | Status |
|---|---|---|
| SVD frequency analysis | Identical math (Q, R, discriminant) | Match |
| Fractal level selection | log2(spacing) + floor() | Match |
| Sublayer calculation | lerp(0.25*dotsTotal, dotsTotal, 1-f) | Match |
| Dither pattern generation | Procedural circular dots (replaces 3D texture) | Adapted |
| Contrast application | Same formula with adjusted multiplier (0.15 vs 0.1) | Calibrated |
| Brightness ramp | Simplified (no ramp texture lookup) | Adapted |
| Radial compensation | gbufferProjection instead of UNITY_MATRIX_P | Adapted |
GetDither3DAltUV | Identical derivative comparison logic | Match |
| CMYK conversion | Identical math | Match |
| UV rotation | Identical | Match |
Known differences (intentional adaptions for Minecraft):
0.15 vs 0.1 (compensates for procedural pattern difference)contrastFade formula: 1/(1+c*0.5) vs 1.05/(1+c) (recalibrated for procedural dots)python tests/test_math.py
======================================================================
Dither3D Mathematical Function Tests
======================================================================
--- SVD Frequency Computation (matches dither3d_core.glsl) ---
PASS Isotropic test: maxFreq=2.000, minFreq=2.000, stretch=1.000
PASS Anisotropic test: maxFreq=4.000, minFreq=1.000, stretch=0.250
PASS Rotated test: maxFreq=2.000, minFreq=2.000, stretch=1.000
--- CMYK Color Conversion ---
PASS Red, Green, Blue, Yellow, Magenta, Cyan, White, Black (8/8)
--- Bayer Matrix Generation ---
PASS Bayer 1x1, 2x2, 4x4, 8x8 (4/4)
======================================================================
All tests passed!
======================================================================
| Category | Count |
|---|---|
| Critical bugs fixed | 1 (sky double exposure) |
| Invalid tests fixed | 1 (SVD algorithm mismatch) |
| Code clarity improvements | 1 (dotRadius formula) |
| Default value changes | 1 (PALETTE_COLOR_MATCH 0→1) |
| Files audited | 37 (lib + gbuffers + composite + final + config) |
| Architecture issues found | 0 |
Verdict: Codebase is stable, well-structured, and faithful to the original algorithm.

Pixelated, retro, Dark Fantasy, gradient, B&W, RGB and more