色フォーマット
標準的な色フォーマット、およびSNESで使用される特殊な色フォーマットについて説明します。

色の混合と色の三要素
物理的には、光の波長の違いによって色が作り出されており、白色光はすべての色の光の集合であるとされます。
人間はふつうは赤、青、緑の3つの色覚を持ち、それら3つの光を各々の強度を適切に操作して混合することで、人間が知覚可能なすべての色を作り出すことができます。この色の作り方は、別の色の光を足していくことから加法混合と呼ばれ、赤青緑の3色を光の三原色と言います。加法混合では赤青緑の3色を同じ強度で混合することでグレースケールを得ることができます。
一方で絵の具や染料を混ぜて色を作る場合は、元の染料の反射光の一部を遮っていくような形で色を作っていきます。したがってこれは減法混合と呼ばれ、理想的な減法混合では加法混合とは逆に黒を作り出すことができます(どんどん光がなくなっていくのだから、完全に光を遮ると黒になる)。減法混合で用いられるシアン、マゼンタ、イエローを色の三原色と言います。 なお、減法混合ではその原理上白色を表現することはできません。

さて、色の見え方は当然発色体によってさまざまに変化しますが、どのような色にも共通するものがあります。色合いを表す色相、濃淡(強度)を表す彩度、そして明るさを表す明度の3つです。これを色の三要素と言います。ただし、白色、灰色、黒色は明度だけで区別され、色相はなく彩度も0となります。白色光の強度を落としていって暗くしていくことを想像すれば理解できると思います。
RGBカラーモデル
RGBカラーモデルは、光の三原色を用いた加法混合を再現した色フォーマットです。SNESでもこのRGBが使用されています。
RGBカラーモデルでは、各原色の明度をどれだけ精細に記録するかという点で多数のフォーマットがあります。

1. RGB888/RGBA8888フォーマット
もっとも標準的なRGBフォーマットで、24ビットカラー、32ビットカラー、トゥルーカラーなどと呼ばれることもあります。各原色の明度を1バイトで表現し、したがって256階調のグラデーションを表現することができます。RGBA8888では、RGB888に加え、透明度情報を記録するアルファチャンネルが追加され、これも8ビットの幅を持ちます。

2. RGB565フォーマット
赤、青の明度を5ビット、緑の明度を6ビット、計16ビットで表現する方法で、65536色を表現することができます。緑のみ64階調、赤、青は32階調となり、この方法でも一般的な色彩表現には十分な色を作ることができます。緑だけ1ビット多いのは、人間の色覚が特に緑に敏感に反応するという生理学的な要因から来ています。
RGB565をRGB888に変換する場合は、次のように正規化を行います。
public static System.Drawing.Color Rgb565ToRgb888(int rgb565)
{
    int red = rgb565 & 0x1F;
    int green = (rgb565 >> 5) & 0x3F;
    int blue = (rgb565 >> 11) & 0x1F;
    
    // Normalization
    red = (red << 3) | (red >> 2);
    green = (green << 2) | (green >> 4);
    blue = (blue << 3) | (blue >> 2);

    return System.Drawing.Color.FromArgb(255, red, green, blue);
}

このように正規化を施す理由は、単純に各色の輝度情報を8倍または4倍しただけではRGB888での最大輝度(255)を得られず、画像全体が暗く見えてしまうためです。

3. RGB555フォーマット
基本的にRGB565フォーマットと同じで、こちらは赤緑青とも5ビット(32階調)で表現します。合計15ビットとなり、2バイトとするには1ビット半端が出ますが、これは使われずそのまま処分されます。RGB555もRGB888に変換する場合には正規化を行います。正規化の式はRGB565での赤および青と同じです。
SNESでは、標準ではRGB555フォーマットの色が使用されます。この時、構造化の方式は下位ビットから順に赤、緑、青が5ビットずつとなります。MSBは常に0とされます。

HSL色空間
HSL色空間は、色相(Hue)、彩度(Saturation)、輝度(Brightness)の3要素を以て色を表現する方法です。RGBが光の三原色による加法混合を再現したものであるのに対し、HSLは色の三要素を組み合わせて指定します。
RGBでは各原色を変化させた時、どういった色合いになるのかが分かりづらく、色づくりが難しいという欠点がありますが、HSLでは、人間が直感的にとらえやすい色合い、鮮やかさ、明るさという色の三要素で指定するため、細かな色の調整がしやすいという利点があります。ただし、その分RGBへの変換はやや煩雑となります。

HSLでの各成分の値は次のようになります。
1. 色相
色相は、色合いを表すパラメータで、一般的には色相環の連想により0°から360°の範囲で表現されます。
各色相における色は次のようになります。彩度は100%、輝度は50%としています。

色相 色見本 RGB
 
(255, 0, 0)
30°
 
(255, 127, 0)
60°
 
(255, 255, 0)
90°
 
(127, 255, 0)
120°
 
(0, 255, 0)
150°
 
(0, 255, 127)
180°
 
(0, 255, 255)
210°
 
(0, 127, 255)
240°
 
(0, 0, 255)
270°
 
(127, 0, 255)
300°
 
(255, 0, 255)
330°
 
(255, 0, 127)
360°
 
(255, 0, 0)

2. 彩度
彩度は色の鮮やかさを示すパラメータで、0%から100%の間で指定されます。100%の時原色となり、0%に向かっていくに従い灰色に収束します。
また、彩度の変化は、RGBの最も強い成分と最も弱い成分の中間に向かうように変化します。
彩度のおおよその目安は次のようになります。色相は0(赤)、輝度は50%としています。

彩度 色見本 RGB
100%
 
(255, 0, 0)
90%
 
(242, 12, 12)
80%
 
(229, 25, 25)
70%
 
(216 38, 38)
60%
 
(204, 51, 51)
50%
 
(191, 63, 63)
40%
 
(178, 76, 76)
30%
 
(165, 89, 89)
20%
 
(153, 102, 102)
10%
 
(140, 114, 114)
0%
 
(127, 127, 127)

3. 輝度
色相で定義された色の明るさを示すパラメータで、0%から100%の間で指定されます。50%の時原色となり、100%に向かうに従い白に、0%に向かうに従い黒に収束します。
また、輝度はRGBのうち最も大きい値と最も小さい値の中間で定義されます。
輝度のおおよその目安は次のようになります。色相は0(赤)、彩度は100%としています。

輝度 色見本 RGB
100%
 
(255, 255, 255)
90%
 
(255, 204, 204)
80%
 
(255, 153, 153)
70%
 
(255 101, 101)
60%
 
(255, 50, 50)
50%
 
(255, 0, 0)
40%
 
(204, 0, 0)
30%
 
(153, 0, 0)
20%
 
(102, 0, 0)
10%
 
(51, 0, 0)
0%
 
(0, 0, 0)

HSLからRGBへの変換、あるいはその逆は次のように行います。
/// <summary>
/// Convert HSL color model to RGB888 color model.
/// </summary>
/// <returns>
/// RGB888 value. (red, green, blue)
/// </returns>
/// <exception cref="System.ArgumentException">
/// Hue is less than 0 or greater than 360, or saturation or brightness is less than 0 or greater than 100.
/// </exception>
public static (int, int, int) HslToRgb888(int hue, int saturaiton, int brightness)
{
    if (hue < 0 || hue > 360) throw new System.ArgumentException("Hue value must be in 0 to 360.");
    if (saturation < 0 || saturation > 100) throw new System.ArgumentException("Saturation value must be in 0 to 100.");
    if (brightness < 0 || brightness > 100) throw new System.ArgumentException("Brightness value must be in 0 to 100.");

    int red, green, blue;
    double max, min;

    if (brightness < 50)
    {
        max = 2.55 * (brightness + brightness * saturation / 100.0);
        min = 2.55 * (brightness - brightness * saturation / 100.0); 
    }
    else
    {
        max = 2.55 * (brightness + (100 - brightness) * saturation / 100.0);
        min = 2.55 * (brightness - (100 - brightness) * saturation / 100.0); 
    }

    if (hue < 60)
    {
        red = (int)max;
        blue = (int)min;
        green = (int)((hue / 60.0) * (max - min) + min);
    }
    else if (hue < 120)
    {
        green = (int)max;
        blue = (int)min;
        red = (int)(((120 - hue) / 60.0) * (max - min) + min);
    }
    else if (hue < 180)
    {
        green = (int)max;
        red = (int)min;
        blue = (int)(((hue - 120) / 60.0) * (max - min) + min);
    }
    else if (hue < 240)
    {
        blue = (int)max;
        red = (int)min;
        red = (int)(((240 - hue) / 60.0) * (max - min) + min);
    }
    else if (hue < 300)
    {
        blue = (int)max;
        green = (int)min;
        red = (int)(((hue - 240) / 60.0) * (max - min) + min);
    }
    else
    {
        red = (int)max;
        green = (int)min;
        blue = (int)(((360 - hue) / 60.0) * (max - min) + min);
    }

    return (red, green, blue);
}

/// <summary>
/// Convert RGB888 color model to HSL color model.
/// </summary>
/// <returns>
/// HSL value. (hue, saturation, brightness)
/// </returns>
/// <exception cref="System.ArgumentException">
/// Red, green or blue is less than 0 or greater than 255.
/// </exception>
public static (int, int, int) Rgb888ToHsl(int red, int green, int blue)
{
    if (red < 0 || red > 255) throw new System.ArgumentException("Red value must be in 0 to 255.");
    if (green < 0 || green > 255) throw new System.ArgumentException("Green value must be in 0 to 255.");
    if (blue < 0 || blue > 255) throw new System.ArgumentException("Blue value must be in 0 to 255.");

    int hue, saturation, brightness, max, min;

    if (red == green && green == blue)
    {
        hue = 0;
        max = min = red;
    }
    else if (red >= green && red >= blue)
    {
        max = red;
        min = System.Math.Min(green, blue);
        hue = 60 * ((green - blue) / (max - min));
    }
    else if (green >= red && green >= blue)
    {
        max = green;
        min = System.Math.Min(red, blue);
        hue = 60 * ((blue - red) / (max - min)) + 120;
    }
    else
    {
        max = blue;
        min = System.Math.Min(red, green);
        hue = 60 * ((red - green) / (max - min)) + 240;
    }

    while (hue < 0) hue += 360;

    brightness = (max + min) / 2;
    if (brightness < 128) saturation = (max - min) / (max + min);
    else saturation = (max - min) / (510 - max - min);

    return (hue, saturation, brightness);
}
ダイレクトカラー
ダイレクトカラー(RGB332)はRGBカラーモデルに基づく色空間です。
8ビットで色を表現する方式で、256色を表現できます。RGに3ビット、Bに2ビットを割り当てることになっています。Bが4階調と少ないのは、人間の視覚が青色の変化に鈍感であるという生理学的理由から来ています。

SNESでは、PPU画面モードが3, 4, 7の場合に、ダイレクトカラーモードを有効にすることで、256色BG1に使用する色として通常のRGB555に代わってダイレクトカラーを使用することができます。ただし、SNESのダイレクトカラーはRGB443という拡張が施されています。

ダイレクトカラーを使用する場合、キャラクタデータにおいてはパレットインデクスの代わりにRGB332方式のダイレクトカラーを指定し、タイルマップのパレット選択ビットはダイレクトカラーの拡張ビットとして使用されます。ダイレクトカラーモード時の色デコードの処理は次のようになります。 なお、SNESのダイレクトカラーモードでは完全な黒色は表現できません。色(0, 0, 0)は透明色として解釈され、黒色は(2, 0, 0), (0, 2, 0), (0, 0, 4)のいずれかで代用することになります。
public static System.Drawing.Color DecodeDirectColor(byte color, ushort tilemap)
{
    /* SNES Direct Color
     * color = BBGGGRRR
     * aditionalColorBits = bgr
     * red = RRRr0
     * green = GGGg0
     * blue = BBb00
     * */
    int additionalColorBits = (tilemap >> 10) & 0x7;
    int red = color & 0x7;
    int green = (color >> 3) & 0x7;
    int blue = (color >> 6) & 0x3;
    red |= additionalColorBits & 0x1;
    green |= (additionalColorBits >> 1) & 0x1;
    blue |= (additionalColorBits >> 2) & 0x1;
    red <<= 1;
    green <<= 1;
    blue <<= 2;
    
    // Normalization
    red |= red >> 2;
    green |= green >> 2;
    blue |= blue >> 2;

    return System.Drawing.Color.FromArgb(255, red, green, blue);
}