ディザリングの技法:1ビットで描く美しい画像
白と黒の2色だけで陰影を表現するディザリングのアルゴリズムを解説。Floyd-Steinbergからブルーノイズまで、実装例とともに紹介します。

Lucas PopeのReturn of the Obra Dinnは、視覚表現の面で最も印象的なゲームの一つですが、使っている色は黒と白の2色だけです。ピクセルあたり1ビット。グレーもグラデーションもアンチエイリアスもなく、すべてのピクセルはオンかオフのどちらかです。それでもこのゲームは、認識できるキャラクター、奥行き感、雰囲気のある照明を備えたフル3Dの世界を描き出しています。そのすべてが、巧みなディザリングによるものです。
ディザリングとは、白と黒のピクセルをパターンとして配置し、目の錯覚によって中間の明るさに見せる技法です。コンピュータグラフィックスの黎明期から使われてきた古い技術ですが、ゲーム開発、Webデザイン、そしてe-inkスクリーンに画像を表示するという現代的な課題で再び注目を集めています。その背後にあるアルゴリズムはエレガントで驚くほど多様であり、思っている以上に応用範囲が広いものです。
なぜディザリングは機能するのか
人間の視覚は個々のピクセルを処理しているわけではなく、領域単位で情報を統合しています。黒と白のピクセルが交互に並んだパターンを見ても、一つひとつの点は見えず、グレーに見えます。白いピクセルが領域の25%を占めていれば25%のグレーに、75%なら75%のグレーに見えます。つまり、領域内の白いピクセルの密度が、知覚される明るさを決めているのです。
これは新聞の網点印刷と同じ原理です。大きさや間隔の異なる点を黒インクで白い紙に印刷することで、連続的な階調を錯覚させています。ディザリングは、点の大きさの代わりにピクセルのパターンを使う、デジタル版の手法といえます。
難しいのはグレースケールを作ること自体ではありません。それは簡単です。難しいのは、視覚的なアーティファクトを出さずにそれを行うことです。パターンの切り替わりで生じるバンディング、線が見えてしまう方向性のバイアス、ディザリングと表示グリッドの干渉によるモアレ、これらを避ける必要があります。
しきい値ディザリング:素朴なアプローチ
最もシンプルなディザリングは、各ピクセルの明るさをしきい値(通常は50%)と比較する方法です。しきい値より明るければ白、暗ければ黒にします。これにより、階調のない高コントラストなシルエットが得られます。テキストや線画には有効ですが、写真や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)
順序ディザリング:グリッド上のパターン
順序ディザリングは、繰り返しタイルの中で位置ごとにしきい値を変えるしきい値行列(ベイヤー行列とも呼ばれます)を使います。すべてのピクセルを同じしきい値と比較するのではなく、各ピクセルの位置に応じて行列から異なる値を取り出して比較します。
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)
順序ディザリングは、一目でわかる特徴的なクロスハッチ模様を生み出します。低解像度では規則的なグリッド構造が見えるため、機械的でレトロな印象を与えます。多くの場面では、これは意図的な美的選択でもあります。8×8のベイヤー行列による秩序だったパターンは独特の見た目があり、多くのゲームやアーティストがあえて採用しています。
ベイヤー行列は、しきい値のレベルが上がるたびに、既存の点からできるだけ離れた位置に新しいピクセルが加わるよう慎重に構成されています。これにより塊ができにくく、どの明るさでも点が最も均等に分布します。また、ピクセルごとの比較は一回だけで隣接ピクセルへの依存もないため非常に高速で、リアルタイムレンダリングにも適しています。
誤差拡散:Floyd-Steinbergとその仲間たち
誤差拡散ディザリングは、まったく異なるアプローチを取ります。固定のしきい値行列を使う代わりに、ピクセルを順番に処理し、元のピクセル値と量子化後の結果(黒か白)との差である「誤差」を、まだ処理していない隣接ピクセルに振り分けます。
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ディザリングは、順序ディザリングよりはるかに自然な仕上がりになります。ピクセルの分布に目立つグリッドパターンがなく、滑らかなグラデーションも見事に表現します。点が有機的にまとまるため、写真の網点印刷に近い見た目になります。
欠点は、逐次的な処理であることです。各ピクセルは、先に処理されたピクセルの誤差に依存するため、並列化が容易ではありません。そのため順序ディザリングより遅く、シェーダーベースのレンダリングパイプラインへの実装も難しくなります。事前に処理する画像なら問題になりませんが、60fpsのリアルタイム3Dレンダリングでは大きな問題になります。
ブルーノイズディザリング:両者の良いとこ取り
ブルーノイズディザリングは、多くの用途で現在の最先端といえる手法です。順序ディザリングと同様にしきい値行列を使うため、高速で並列化も可能です。ただし行列の値はブルーノイズ分布になるように配置されています。つまり点はランダムに配置されつつも最小間隔の制約を持ち、ホワイトノイズの塊と順序ディザリングのグリッドパターンの両方を避けます。
「ブルーノイズ」という言葉は信号処理に由来します。ブルーノイズはエネルギーの大半が高周波側にあり、青い光のようなものです。空間的に言えば、大きな構造(目に見えるグリッドや方向性のバイアス)がなく、局所的には点が均等に散らばっている(塊も隙間もない)パターンになります。ダーツ盤に、どのダーツも他のダーツから必ず一定の最小距離以上離れて刺さるという制約をつけて投げたときにできるパターンに近いものです。
良質なブルーノイズのしきい値行列を生成するのは、驚くほど難しい作業です。周波数スペクトルが目的のプロファイルに一致するまで反復計算する、最適化問題になります。しかし一度行列ができてしまえば、使うのはベイヤーディザリングと同じくらい高速で、ピクセルごとの比較は一回だけです。
Obra Dinnのアプローチ:球面マッピングディザリング
Return of the Obra Dinnのディザリングは特に巧妙です。3Dレンダリング特有の問題、つまり時間的安定性を解決しているからです。標準的なディザリングパターンをスクリーン空間に適用すると、カメラを動かしたときにパターンが「泳いで」しまいます。パターンは画面に固定されたまま3Dジオメトリだけが下で動くため、非常に気になるちらつきが生じます。
Popeの解決策は、ディザリングのパターンをスクリーン空間ではなく3D空間にマッピングすることでした。ディザリングパターンはカメラを囲む球面に投影され、各ピクセルの視線方向に応じてサンプリングされます。カメラが回転すればパターンも一緒に回転します。オブジェクトがワールド空間で動いても、そのディザリングはスクリーン上の位置ではなくワールド空間の位置から導出されるため、安定したままです。
これは、ディザリングや網点効果を使うあらゆる3Dアプリケーションに応用できるテクニックです。重要なポイントは、時間的安定性が欲しいなら、しきい値はフレームごとに変わるもの(スクリーン座標)ではなく、安定したもの(ワールド位置や視線方向)の関数にすべきだということです。
レトロな美的表現を超えた実用的な応用
ディザリングはレトロゲームの見た目のためだけのものではありません。現代の開発においても、実用的な応用があります。
- E-inkディスプレイ。 E-readerやe-inkサイネージのグレースケール表現力は限られており(通常16段階)、写真やグラデーションを表示するにはディザリングが不可欠です。どのディザリングアルゴリズムを選ぶかで、読みやすさと画質が直接左右されます。
- ファイルサイズの削減。 1ビットのディザリング画像は、8ビットのグレースケール画像の生データのわずか8分の1です。Webで装飾用の画像を配信する場合、ディザリングしたPNGはグレースケールの代替品よりも大幅に小さくできます。見た目とパフォーマンスの両方の理由から、あえてディザリング画像を使うWebデザイナーもいます。
- 色の量子化。 フルカラー画像を限られたパレット(GIF、インデックスPNG、パレット制約のあるディスプレイ)に減色すると、滑らかなグラデーションが離散的な色値に割り当てられることでバンディングが起きます。ディザリングはこれを防ぎます。色空間でのFloyd-Steinbergディザリングは、GIFエンコーダーがこの問題を処理する方法です。
- レンダリングにおけるグラデーションのバンディング。 色深度の低いレンダーターゲット(チャンネルあたり8ビット)では、滑らかなグラデーションに目立つバンディングが生じることがあります。スクリーン空間にディザリングを加えれば(値に対して±0.5程度でも)、見えるノイズを出さずにバンディングを崩せます。現代の主要なゲームエンジンの多くは、これを自動的に行っています。
- サーマルプリンターとレシート印刷。 サーマルプリンターは本質的に1ビットです。各ドットは加熱される(黒)か、されないかのどちらかです。ディザリングによって写真やグラフィックを印刷可能な形に変換します。どのアルゴリズムを選ぶかで、レシートの画像が認識できるか、ただの塊になるかが決まります。
シェーダーにおけるディザリングの実装
リアルタイムのアプリケーションでは、順序ディザリングとブルーノイズディザリングを、フラグメントシェーダーとして効率的に実装できます。
// 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);
}
ブルーノイズの場合は、通常ノイズテクスチャを事前に計算しておき、シェーダーでサンプリングします。テクスチャはタイル状に並べられるため、64×64や128×128のブルーノイズテクスチャを繰り返せば画面全体をカバーできます。ピクセルあたりのコストはテクスチャのサンプル1回と比較1回だけで、現代のGPUではほぼ無料といえます。
適切なアルゴリズムの選び方
どのディザリング手法にも、それが最適となるユースケースがあります。
- しきい値: テキスト、線画、階調の幅よりも細部が重要な高コントラストのグラフィック。
- 順序(ベイヤー): 意図的なレトロ・網点風の見た目を狙うリアルタイムレンダリング。高速で並列化しやすく、GPUに適しています。
- Floyd-Steinberg: 品質が最優先となる静的な画像処理。写真や滑らかなグラデーションを含む画像に最適です。
- ブルーノイズ: 目立つパターンを出さずに自然な仕上がりを求めるリアルタイムレンダリング。現代のアプリケーションにおける最も汎用的な選択肢です。
- 球面・ワールド空間: 時間的安定性が重要な3Dレンダリング。Obra Dinnのような1ビットの3Dレンダリングには欠かせません。
ディザリングは、「点を並べて陰影に見せる」という概念のシンプルさとは裏腹に、実装上の選択肢が非常に奥深いトピックです。各アルゴリズムは、品質・速度・視覚的な個性の間の異なるトレードオフを表しています。e-inkディスプレイ、低帯域の接続、レトロな美学がいずれも関係する現代においては、こうしたトレードオフを理解することがこれまで以上に実用的になっています。


