RGBHSV変換を実装してみた
まずRGB->HSV
float3 rgb2hsv(float3 rgb) { float3 hsv; float rgb_max, rgb_min; float diff; float base; float div1; float div2; rgb_max = max(max(rgb.x, rgb.y), rgb.z); rgb_min = min(min(rgb.x, rgb.y), rgb.z); if(rgb_max == rgb.x){ diff = rgb.y - rgb.z; // G-B base = (rgb.y < rgb.z)? 360.0f : 0.0f; } if(rgb_max == rgb.y){ diff = rgb.z - rgb.x; // B-R base = 120.0f; } if(rgb_max == rgb.z){ diff = rgb.x - rgb.y; // R-G base = 240.0f; } div1 = (rgb_max == rgb_min)? 1.0f : (rgb_max - rgb_min); div2 = (rgb_max > 0.0f)? rgb_max : 1.0f; hsv.x = 60.0f * diff / div1 + base; hsv.y = (rgb_max - rgb_min) / div2; hsv.z = rgb_max; return hsv; }
次。HSV->RGB
float3 hsv2rgb(float3 hsv) { float3 rgb; int Hi; float f; float p; float q; float t; Hi = fmod(floor(hsv.x / 60.0f), 6.0f); f = hsv.x / 60.0f - Hi; p = hsv.z * (1.0f - hsv.y); q = hsv.z * (1.0f - f * hsv.y); t = hsv.z * (1.0f - (1.0f - f) * hsv.y); if(Hi == 0){ rgb.x = hsv.z; rgb.y = t; rgb.z = p; } if(Hi == 1){ rgb.x = q; rgb.y = hsv.z; rgb.z = p; } if(Hi == 2){ rgb.x = p; rgb.y = hsv.z; rgb.z = t; } if(Hi == 3){ rgb.x = p; rgb.y = q; rgb.z = hsv.z; } if(Hi == 4){ rgb.x = t; rgb.y = p; rgb.z = hsv.z; } if(Hi == 5){ rgb.x = hsv.z; rgb.y = p; rgb.z = q; } return rgb; }
途中のif文の羅列は本来ならHiでswitch-caseするところだけどそっちだと10命令以上処理が増えてしまう。われわれ人間はHiの条件がどれか1つしか同時に成立しない(かつ、どれか1つは必ず成立する)ことを分かっているのでif文の羅列に変換。
単純にRGB値をHSVに変換して何もせず元に戻した値をテクスチャへ書き込むって処理なら54命令になった。
正直そんなに軽い処理ではない。劇重ってほどひどくもないけど。
で、ここからが今回のメイン。
相互変換できるようになったけど、「floatで計算誤差は大丈夫?」って思うわけで検証してみた。
まず机上?でC++で動かして誤差を確認してみた。
int main(void) { const float div = 1.0f / 255.0f; for(int r=0;r<256;r++){ for(int g=0;g<256;g++){ for(int b=0;b<256;b++){ float3 val(r*div, g*div, b*div); mycmp256( val, hsv2rgb(rgb2hsv(val)) ); } } } }
シェーダは0.0〜1.0でRGBの0〜255を表現するので1/255ずつ増やしながらすべての組み合わせで誤差を確認してみた。float3とかはC++には無いので独自classで擬似ってみた。(ソースは省略)
比較ルーチンはこんな感じ。
bool cmp_float1(float lhs, float rhs) { // 許容する最小誤差。0.0f なら誤差が少しでもあればひっかかる // 差分が出る儀る場合に調整する const float e = 0.0f;//1.0f/128.0f; return ((lhs-e) <= rhs) && ((lhs+e) >= rhs); } bool cmp_float3(const float3 &lhs, const float3 &rhs) { return cmp_float1(lhs.x, rhs.x) && cmp_float1(lhs.y, rhs.y) && cmp_float1(lhs.z, rhs.z); } bool cmp256(const float3 &lhs, const float3 &rhs) { // float -> UNORM への変換誤差をシミュレートするために 255 倍する return cmp_float3(255.0f * lhs, 255.0f * rhs); } void printdiff256(const float3 &lhs, const float3 &rhs) { printf("(%d, %d, %d) -> %s\n", (int)(255.0f * lhs.x), (int)(255.0f * lhs.y), (int)(255.0f * lhs.z), (255.0f * lhs - 255.0f * rhs).toString().c_str() // RGB値(0〜255)での差分を表示する ); } void mycmp256(const float3 &lhs, const float3 &rhs) { if(!cmp256(lhs, rhs)){ printdiff256(lhs, rhs); } }
で、実行すると
(0, 1, 115) -> (0.0000000000000000, -0.0000079870223999, 0.0000000000000000)
(0, 1, 116) -> (0.0000000000000000, 0.0000011324882507, 0.0000000000000000)
みたいな感じでわらわらと出力される。
e = 1.0f/128.0f とすると何も出力されない。
float <-> UNORM 変換は 255 の乗算除算なので、半分の128でひっかからないなら原理的には誤差が見えないはず。
で、試しにどこまで大丈夫か許容誤差を引き上げていくと
e = 1.0f / pow(2.0f, 13.0f) まで(2^-13)は大丈夫で2^-14でちらほらと出力された。
ということで、計算誤差は 2^-13 っぽい。*2
ただ、誤差は+方向にも−方向にも出ていて単純に255倍して少数以下を破棄すると異なるRGB値(0〜255)となってしまう。次回以降でROPのfloat->UNORM変換が単純な「切り捨て」ではなく、「近い数値へ丸め」となっているかどうかを検証する。