
Dithering3D
Pixelated, retro, Dark Fantasy, gradient, B&W, RGB and more
Dither3D Shader — Audit & Fix Report
Date: 2026-05-15
Scope: Full codebase integrity review against original
Dither3DInclude.cginc
1. BUG FIXES
1.1 — CRITICAL: Sky double exposure/offset
File:
shaders/gbuffers_skybasic.fsh:37Severity: High — visual blowout at any non-default exposure/offsetapplyDither3DColor()already appliesDITHER_EXPOSURE * color + DITHER_OFFSETinternally (line 53 ofdither3d_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.fshhad this issue.1.2 — MEDIUM: Invalid SVD frequency test
File:
tests/test_math.py:15-30Severity: Medium — test was validating a different algorithm than the shader usesThe Python
compute_uv_frequency()computedsqrt(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:
- Isotropic (uniform UV scale): both singular values equal scale ✓
- Anisotropic (stretched UV): singular values equal individual axis scales ✓
- Rotated (45°): singular values invariant under rotation ✓
Also fixed: Unicode characters (checkmarks, arrows) replaced with ASCII for Windows console compatibility.
2. CODE QUALITY IMPROVEMENTS
2.1 — Clarified dotRadius formula
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), giving0.125for 16 dots and0.25for 4 dots.2.2 — PALETTE_COLOR_MATCH default changed to Color-Aware
Files:
shaders/lib/dither3d_options.glsl:41,shaders/lib/dither3d_config.glsl:63#define PALETTE_COLOR_MATCH 1 // was 0Color-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.
3. ARCHITECTURE VERIFICATION
3.1 — Include chain (correct)
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.glslDITHER3D_OPTIONS_GLSLdither3d_config.glslDITHER3D_CONFIG_GLSLdither3d_core.glslDITHER3D_CORE_GLSLdither3d_utils.glslDITHER3D_UTILS_GLSLdither3d_palettes.glslDITHER3D_PALETTES_GLSLdither3d_color.glslDITHER3D_COLOR_GLSL3.2 — Vertex shaders (all 14 pairs verified)
All vertex shaders correctly compute and pass
screenPos = gl_Positionfor radial compensation. World position and normal calculations are consistent across all geometry types:- Terrain, Entities, Block, Hand, Water: triplanar UV with world normal
- Basic, Clouds, Weather, Armor Glint, Spider Eyes: simple world UV (no normal available)
- Sky Basic: cylindrical UV with view direction and alternate UV seam handling
- Sky Textured: texture atlas UV (scaled x4 for dithering)
- Beacon Beam: XZ plane with vertical offset
3.3 — Fragment shaders (all 14 pairs verified)
All fragment shaders follow the same pattern:
- Include
dither3d_options.glsl(first, defines macros) - Include
dither3d_color.glsl(pulls in everything) - Sample textures, compute color
- Call
applyDither3DColorSimple()orapplyDither3DColor() - Output to
gl_FragData[0]with proper alpha preservation
3.4 — Shader properties (validated)
shaders.properties: 14RENDER_STYLEvalues, 8 profiles, proper slider ranges, nested submenu structurelang/en_US.lang: all options, values, profiles, and screen names have labels- Custom palette sliders (colors 1-8, 24 RGB sliders) all wired through
shaders.properties→dither3d_options.glsl→dither3d_config.glsl
3.5 — Palette system (validated)
- 9 built-in palettes: 1-Bit(2), GameBoy(4), CGA(4), VirtualBoy(4), Sepia(4), Nord(8), Solarized(8), Pico-8(16), Eldritch(16)
- 3 custom modes: 2-color, 4-color, 8-color via user-adjustable RGB sliders
- Dual matching: luminance-based (fast, crisp) and color-aware (preserves hues)
- All palettes sorted by luminance for correct dithering gradient
getPaletteColor()centralizes all access withclamp()bounds safety
3.6 — CMYK halftone (validated)
- 4 plates with correct traditional screen angles (15°, 75°, 0°, 45°)
- Slight scale variation per plate breaks up moire patterns
- Round-trip conversion matches original HLSL (validated by test)
4. ORIGINAL PORT FIDELITY
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 gbufferProjectioninstead ofUNITY_MATRIX_PAdapted GetDither3DAltUVIdentical derivative comparison logic Match CMYK conversion Identical math Match UV rotation Identical Match Known differences (intentional adaptions for Minecraft):
- No 3D texture: replaced with procedural Bayer-pattern circular dots
- No brightness ramp texture: direct clamping used instead
- Contrast multiplier:
0.15vs0.1(compensates for procedural pattern difference) contrastFadeformula:1/(1+c*0.5)vs1.05/(1+c)(recalibrated for procedural dots)- RENDER_STYLE 0 (RGB) uses small UV offsets instead of original's per-channel dithering
5. TEST RESULTS
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! ======================================================================
6. SUMMARY
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.
Test version:
- Addition of a visual style selection system (color palettes)
- Addition of several color palette modes for a more personalized look
- Addition of a system for creating your own color palette up to 8 bits (8 colors)
- Menu improvements
- Performance optimization
- And more...
Нет описания изменений
