
Transform your Minecraft world with Surface-Stable Fractal Dithering — a groundbreaking rendering technique where dither dots stick to 3D surfaces instead of the screen, creating a unique manga/comic book aesthetic that feels alive.
🎬 Based on the innovative work by Rune Skovbo Johansen — Watch the explainer video
Unlike traditional dithering where patterns "swim" on screen, Dither3D anchors each dot to the actual 3D surface. Walk around blocks, and the dots stay perfectly attached!
As you move closer or farther from surfaces, dots dynamically split or merge to maintain constant screen-space density. It's mesmerizing to watch!
| Mode | Description |
|---|---|
| Grayscale | Classic black & white dithering — clean, minimalist, timeless |
| RGB | Each color channel gets its own dot layer — vibrant and unique |
| CMYK Halftone | Authentic newspaper/comic print simulation with rotated dot angles |
| Feature | Description |
|---|---|
| 🎯 Precision Dithering | SVD-based frequency analysis for mathematically perfect dot placement |
| 🌀 Anti-Stretch Technology | Anisotropic smoothing keeps dots circular even on oblique surfaces |
| 🎥 Camera Stable | Radial compensation prevents dot swimming during camera rotation |
| ⚙️ Highly Configurable | 10+ parameters to fine-tune your perfect look |
| 🚀 Performance Optimized | Efficient 3D texture sampling for smooth gameplay |
| 📱 Universal Compatibility | Works with OptiFine AND Iris on virtually any Minecraft version |
Fine-tune every aspect of the dithering effect with our intuitive in-game sliders:
| Parameter | Range | Effect |
|---|---|---|
| Dot Scale | 2 - 10 | Control overall dot size (exponential) |
| Size Variability | 0 - 1 | 0 = Bayer pattern, 1 = Halftone style |
| Dot Contrast | 0 - 2 | Sharpen or soften dot edges |
| Stretch Smooth | 0 - 2 | Combat stretched dots on angled surfaces |
| Parameter | Range | Effect |
|---|---|---|
| Exposure | 0 - 5 | Brightness multiplier |
| Offset | -1 to 1 | Brightness offset adjustment |
shaders folder in .minecraft/shaderpacks/shaders folder in .minecraft/shaderpacks/💡 Tip: Press
F3 + Tto quickly reload shaders after making changes!
Choose from pre-configured profiles for instant results:
| Profile | Color Mode | Dot Scale | Best For |
|---|---|---|---|
| 🟢 LOW | Grayscale | 4.0 | Performance, retro feel |
| 🟡 MEDIUM | Grayscale | 5.0 | Balanced experience |
| 🟠 HIGH | RGB | 5.0 | Colorful, artistic |
| 🔴 ULTRA | CMYK | 6.0 | Maximum visual impact |
| 📸 Screenshot enthusiasts | Create unique, artistic captures |
|---|---|
| 🎬 Content creators | Stand out with a distinctive visual style |
| 🎮 Retro lovers | Relive the charm of 1-bit graphics |
| 🎨 Artists | Manga, comic book, and newspaper aesthetics |
| 🧪 Tech enthusiasts | Experience cutting-edge rendering techniques |
Q: Does this work with other shaders?
Dithering3D is a standalone shader pack. Combining with other shaders may cause conflicts.
Q: Why do some surfaces look different?
The sky and some special effects may appear different as surface-stable dithering reveals flat geometry.
Q: Performance impact?
Moderate. The 3D texture sampling is optimized but more demanding than vanilla rendering. Most GPUs handle it smoothly.
Q: Can I use this in my modpack?
Yes! Under MPL-2.0 license. Credit appreciated.
Dithering3D uses Singular Value Decomposition (SVD) to analyze UV coordinate derivatives in real-time, determining the exact frequency and direction of surface textures. This mathematical approach enables:
✅ Perfectly uniform dot density regardless of distance
✅ Seamless fractal transitions between detail levels
✅ Circular dots even on extremely angled surfaces
✅ Rotation-stable patterns that don't "swim"
| Resource | Link |
|---|---|
| 🔗 Original Algorithm | Rune Skovbo Johansen |
| 🎥 Technique Explanation | YouTube Video |
| 💬 Technical Discussion | FAQ Thread |
Mozilla Public License 2.0 (MPL-2.0)
🎨 Transform your world. Experience Dithering3D. 🎨
If you enjoy this shader, consider leaving a ⭐ and sharing this shader page !
Required
Unsupported
Terms for using, modifying and redistributing this product.
15.05.2026 · Modrinth
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...
Screenshots and images from the original Modrinth page.










Published versions and original Modrinth files.
15.05.2026 · 5 852 downloads
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:
- 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.
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:
- 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
All fragment shaders follow the same pattern:
1. Include dither3d_options.glsl (first, defines macros)
2. Include dither3d_color.glsl (pulls in everything)
3. Sample textures, compute color
4. Call applyDither3DColorSimple() or applyDither3DColor()
5. Output to gl_FragData[0] with proper alpha preservation
shaders.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):
- No 3D texture: replaced with procedural Bayer-pattern circular dots
- No brightness ramp texture: direct clamping used instead
- Contrast multiplier: 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)
- RENDER_STYLE 0 (RGB) uses small UV offsets instead of original's per-channel dithering
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.
11.01.2026 · 4 636 downloads
07.01.2026 · 49 downloads