Seni Dithering: Gambar Indah dengan Satu Bit
Cara algoritma dithering menciptakan gradasi hanya dengan piksel hitam-putih, dari Floyd-Steinberg hingga blue noise, plus contoh implementasinya.

Game Return of the Obra Dinn karya Lucas Pope adalah salah satu game paling memukau secara visual yang pernah dibuat, dan ia hanya menggunakan dua warna: hitam dan putih. Satu bit per piksel. Tidak ada abu-abu, tidak ada gradien, tidak ada anti-aliasing. Setiap piksel hanya bisa menyala atau mati. Namun entah bagaimana, game ini tetap merender dunia 3D lengkap dengan karakter yang bisa dikenali, persepsi kedalaman, dan pencahayaan atmosferik, semuanya berkat dithering yang cerdas.
Dithering adalah teknik menyusun piksel hitam dan putih dalam pola yang mengecoh mata agar melihat nuansa warna di antaranya. Teknik ini sudah ada sejak awal grafika komputer, dan sekarang sedang naik daun lagi di pengembangan game, desain web, dan masalah yang makin relevan, yaitu menampilkan gambar di layar e-ink. Algoritma di baliknya elegan, sangat beragam, dan lebih berguna dari yang kamu kira.
Mengapa Dithering Bisa Bekerja
Sistem visual kita tidak memproses piksel satu per satu, melainkan mengintegrasikan area. Saat kamu melihat pola piksel hitam dan putih yang berselang-seling, kamu tidak melihat titik-titik individual. Kamu melihat abu-abu. Area di mana 25% pikselnya putih terlihat seperti abu-abu 25%. Area dengan 75% piksel putih terlihat seperti abu-abu 75%. Kepadatan piksel putih dalam suatu area itulah yang mengendalikan kecerahan yang dirasakan.
Ini prinsip yang sama dengan cetak halftone di koran. Titik-titik dengan ukuran dan jarak yang bervariasi menciptakan ilusi tone kontinu dari tinta hitam murni di atas kertas putih. Dithering adalah padanan digitalnya, yaitu memakai pola piksel sebagai pengganti ukuran titik.
Tantangannya bukan membuat skala abu-abu, karena itu bagian yang mudah. Tantangannya adalah melakukannya tanpa artefak yang terlihat: tidak ada banding (transisi tiba-tiba antar pola), tidak ada bias arah (pola yang membentuk garis-garis terlihat), dan tidak ada efek moiré (pola interferensi antara dithering dan grid layar).
Threshold Dithering: Pendekatan Naif
Dithering paling sederhana: bandingkan kecerahan tiap piksel dengan sebuah ambang (biasanya 50%). Lebih terang dari ambang menjadi putih, lebih gelap menjadi hitam. Hasilnya siluet berkontras tinggi tanpa gradasi sama sekali. Berguna untuk teks dan line art, tapi buruk untuk foto atau rendering 3D.
import numpy as np
from PIL import Image
def threshold_dither(image, threshold=128):
"""Binary threshold — no dithering at all, really."""
gray = np.array(image.convert('L'), dtype=float)
return Image.fromarray((gray > threshold).astype(np.uint8) * 255)
Ordered Dithering: Pola di Atas Grid
Ordered dithering menggunakan matriks ambang (juga disebut matriks Bayer) yang memvariasikan ambang di seluruh pola ubin yang berulang. Alih-alih membandingkan setiap piksel dengan ambang yang sama, setiap piksel dibandingkan dengan nilai berbeda dari matriks berdasarkan posisinya.
def ordered_dither(image, matrix_size=4):
"""Ordered dithering with Bayer matrix."""
gray = np.array(image.convert('L'), dtype=float)
h, w = gray.shape
# 4×4 Bayer matrix (values 0-15, normalized to 0-255)
bayer_4x4 = np.array([
[ 0, 8, 2, 10],
[12, 4, 14, 6],
[ 3, 11, 1, 9],
[15, 7, 13, 5]
]) * (255 / 16)
# Tile the matrix across the image
threshold = np.tile(bayer_4x4, (h // 4 + 1, w // 4 + 1))[:h, :w]
return Image.fromarray((gray > threshold).astype(np.uint8) * 255)
Ordered dithering menghasilkan pola crosshatch yang khas dan mudah dikenali. Pada resolusi rendah, struktur grid yang teratur akan terlihat dan memberi gambar kesan mekanis serta retro. Ini sebenarnya pilihan estetika di banyak konteks. Pola berurutan dari matriks Bayer 8×8 punya tampilan khas yang sengaja dipakai banyak game dan seniman.
Matriks Bayer disusun sedemikian rupa sehingga setiap level ambang menambahkan piksel baru di posisi yang sejauh mungkin dari piksel yang sudah ada. Ini meminimalkan penggumpalan dan menghasilkan distribusi titik yang paling merata di setiap level kecerahan. Caranya juga sangat cepat, yaitu satu perbandingan per piksel tanpa ketergantungan pada tetangga, sehingga cocok untuk rendering real-time.
Error Diffusion: Floyd-Steinberg dan Turunannya
Error diffusion mengambil pendekatan yang sama sekali berbeda. Alih-alih memakai matriks ambang tetap, ia memproses piksel secara berurutan dan menyebarkan 'error', yaitu selisih antara nilai piksel asli dan hasil kuantisasi (hitam atau putih), ke piksel tetangga yang belum diproses.
def floyd_steinberg_dither(image):
"""Floyd-Steinberg error diffusion dithering."""
gray = np.array(image.convert('L'), dtype=float)
h, w = gray.shape
for y in range(h):
for x in range(w):
old_pixel = gray[y, x]
new_pixel = 255.0 if old_pixel > 128 else 0.0
gray[y, x] = new_pixel
error = old_pixel - new_pixel
# Distribute error to neighbors
# Floyd-Steinberg diffusion kernel:
# * 7/16
# 3/16 5/16 1/16
if x + 1 < w:
gray[y, x + 1] += error * 7 / 16
if y + 1 < h:
if x - 1 >= 0:
gray[y + 1, x - 1] += error * 3 / 16
gray[y + 1, x] += error * 5 / 16
if x + 1 < w:
gray[y + 1, x + 1] += error * 1 / 16
return Image.fromarray(gray.astype(np.uint8))
Floyd-Steinberg menghasilkan hasil yang jauh lebih natural dibanding ordered dithering. Distribusi pikselnya tidak punya pola grid yang terlihat, dan ia menangani gradien halus dengan sangat baik. Titik-titiknya mengelompok secara organik, menghasilkan tampilan yang lebih mirip cetak halftone fotografis.
Kelemahannya: prosesnya berurutan. Setiap piksel bergantung pada error dari piksel yang sudah diproses, jadi sulit diparalelkan. Akibatnya lebih lambat dari ordered dithering dan lebih sulit diimplementasikan di pipeline rendering berbasis shader. Untuk gambar yang sudah diproses sebelumnya, ini tidak masalah. Tapi untuk rendering 3D real-time di 60fps, ini jadi masalah.
Blue Noise Dithering: Yang Terbaik dari Dua Dunia
Blue noise dithering adalah standar terkini untuk banyak aplikasi. Ia memakai matriks ambang seperti ordered dithering (jadi tetap cepat dan bisa diparalelkan), tapi nilai matriksnya disusun agar menghasilkan distribusi blue noise. Titik-titiknya tersebar acak namun dengan jarak minimum, sehingga terhindar dari penggumpalan white noise sekaligus pola grid pada ordered dithering.
Istilah 'blue noise' berasal dari pemrosesan sinyal. Blue noise memiliki sebagian besar energinya di frekuensi tinggi, seperti cahaya biru. Dalam ruang, ini berarti polanya tidak punya struktur skala besar (tidak ada grid yang terlihat, tidak ada bias arah), tetapi titiknya tersebar merata di level lokal (tidak ada gumpalan, tidak ada celah). Ini seperti pola yang terbentuk jika kamu melempar panah ke papan dengan syarat setiap panah harus berjarak minimal tertentu dari panah lain.
Membuat matriks blue noise threshold yang bagus cukup sulit. Ini masalah optimasi yang membutuhkan iterasi sampai spektrum frekuensinya sesuai profil yang diinginkan. Tapi begitu matriksnya sudah jadi, pemakaiannya secepat Bayer dithering: satu perbandingan per piksel.
Pendekatan Obra Dinn: Spherical Mapped Dithering
Dithering di Return of the Obra Dinn sangat cerdas karena ia memecahkan masalah yang khas untuk rendering 3D: stabilitas temporal. Saat pola dithering standar diterapkan di screen space, menggerakkan kamera membuat pola dithering 'berenang'. Polanya tetap menempel di layar sementara geometri 3D bergerak di bawahnya. Efek kerlip ini sangat mengganggu.
Solusi Pope: memetakan pola dithering di ruang 3D, bukan di ruang layar. Pola dithering diproyeksikan ke sebuah bola yang mengelilingi kamera, lalu disampel berdasarkan arah pandang untuk setiap piksel. Saat kamera berputar, pola dithering ikut berputar. Saat objek bergerak di ruang dunia, dithering-nya tetap stabil karena diturunkan dari posisi world-space, bukan posisi layar.
Ini teknik yang bisa kamu adaptasi untuk aplikasi 3D apa pun yang memakai efek dithering atau halftone. Intinya: kalau kamu menginginkan stabilitas temporal, nilai ambangmu harus merupakan fungsi dari sesuatu yang stabil (posisi dunia, arah pandang), bukan sesuatu yang berubah setiap frame (koordinat layar).
Aplikasi Praktis di Luar Estetika Retro
Dithering bukan hanya untuk estetika game retro. Ia punya aplikasi praktis yang nyata dalam pengembangan modern.
- Layar e-ink. E-reader dan signage e-ink punya kemampuan skala abu-abu yang terbatas (biasanya 16 level). Dithering wajib dipakai untuk menampilkan foto dan gradien di perangkat ini. Pilihan algoritmanya langsung memengaruhi keterbacaan dan kualitas gambar.
- Mengurangi ukuran file. Gambar dithering 1-bit berukuran 1/8 dari gambar grayscale 8-bit mentah. Untuk pengiriman gambar dekoratif di web, PNG dithering bisa jauh lebih kecil dibanding alternatif grayscale. Beberapa desainer web memakai gambar dithering dengan sengaja, demi estetika sekaligus performa.
- Kuantisasi warna. Saat kamu mengurangi gambar berwarna penuh ke palet terbatas (GIF, PNG indexed, atau layar dengan palet terbatas), dithering mencegah banding yang muncul ketika gradien halus dipetakan ke nilai warna diskret. Floyd-Steinberg di ruang warna adalah cara encoder GIF menangani hal ini.
- Banding gradien dalam rendering. Render target presisi rendah (8-bit per channel) bisa menghasilkan banding yang terlihat pada gradien halus. Menambahkan screen-space dithering (cukup ±0,5 dari nilai) memecah banding tanpa menimbulkan noise yang terlihat. Sebagian besar game engine modern melakukannya secara otomatis.
- Printer thermal dan struk. Printer thermal pada dasarnya 1-bit, karena setiap titik dipanaskan (hitam) atau tidak. Dithering mengubah foto dan grafis menjadi bentuk yang bisa dicetak. Pilihan algoritmanya menentukan apakah gambar di struk kamu bisa dikenali atau cuma jadi gumpalan.
Mengimplementasikan Dithering di Shader
Untuk aplikasi real-time, ordered dithering dan blue noise dithering bisa diimplementasikan secara efisien sebagai fragment shader.
// GLSL fragment shader for Bayer 8x8 ordered dithering
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
// Bayer 8x8 matrix lookup via bit manipulation (no texture needed)
float bayer8x8(vec2 pos) {
ivec2 p = ivec2(mod(pos, 8.0));
int index = p.x + p.y * 8;
// Bit-reversal trick for Bayer matrix values
int value = 0;
int x = p.x ^ p.y;
int y = p.y;
for (int i = 0; i < 3; i++) {
value = value * 4 + (x & 1) * 2 + (y & 1);
x >>= 1;
y >>= 1;
}
return float(value) / 64.0;
}
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
float luminance = dot(color.rgb, vec3(0.299, 0.587, 0.114));
float threshold = bayer8x8(gl_FragCoord.xy);
float dithered = step(threshold, luminance);
gl_FragColor = vec4(vec3(dithered), 1.0);
}
Untuk blue noise, biasanya kamu menghitung tekstur noise terlebih dahulu lalu menyampelnya di shader. Teksturnya bisa di-tile, jadi tekstur blue noise 64×64 atau 128×128 sudah cukup menutupi seluruh layar saat diulang. Biaya per piksel hanya satu sampel tekstur dan satu perbandingan, yang pada GPU modern nyaris gratis.
Memilih Algoritma yang Tepat
Setiap pendekatan dithering punya kasus penggunaan di mana ia menjadi pilihan terbaik.
- Threshold: Teks, line art, dan grafis berkontras tinggi di mana detail lebih penting daripada rentang tonal.
- Ordered (Bayer): Rendering real-time yang menginginkan estetika retro atau halftone yang disengaja. Cepat, bisa diparalelkan, dan ramah GPU.
- Floyd-Steinberg: Pemrosesan gambar statis di mana kualitas paling utama. Terbaik untuk foto dan gambar dengan gradien halus.
- Blue noise: Rendering real-time yang menginginkan hasil natural tanpa pola yang terlihat. Pilihan serba guna terbaik untuk aplikasi modern.
- Spherical/world-space: Rendering 3D di mana stabilitas temporal sangat penting. Wajib untuk rendering 3D 1-bit seperti Obra Dinn.
Dithering adalah salah satu topik yang konsepnya sederhana, yaitu 'susun titik-titik agar terlihat seperti bayangan', tetapi menyimpan kedalaman dalam pilihan implementasinya. Setiap algoritma mewakili trade-off berbeda antara kualitas, kecepatan, dan karakter visual. Di era ketika layar e-ink, koneksi bandwidth rendah, dan estetika retro semuanya relevan, memahami trade-off ini terasa lebih praktis dari sebelumnya.


