はじめに
以前作ったVAT生成の仕組みでpotision texture の alpha に圧縮した法線を格納することでメモリ消費を削減したかったので球面座標を用いた法線圧縮について調べてみました
本記事は以下のWebページのコード(MIT License)を参考に作成されています
実装
検証用のshaderです
half spherical_16(float3 nor)
{
float2 v = float2(0.5 + 0.5 * atan2(nor.z, nor.x) / 3.141593, acos(nor.y) / 3.141593);
uint2 d = uint2(round(v * 255.0));
return d.x | (d.y << 8u);
}
float3 i_spherical_16(half data)
{
uint d = data;
float2 v = float2(d & 255u, d >> 8) / 255.0;
v.x = 2.0 * v.x - 1.0;
v *= 3.141593;
return normalize(float3(sin(v.y) * cos(v.x), cos(v.y), sin(v.y) * sin(v.x)));
}
v2f vert(appdata v)
{
v2f o;
float3 nrm = v.normal;
half enc = spherical_16(nrm);
o.normal = i_spherical_16(enc);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = fixed4(i.normal, 1.0);
return col;
}
左はModelのNormalをそのままfragment shaderで出力したもの
右はModelのNormalを圧縮して解凍したものをfragment shaderで出力したもの(前述の検証用shader)
解説
圧縮
ざっくり説明すると、直交座標系の法線を球面座標に変換して角度を2つ(phi, theta)を各8bitでhalf(16bit)に格納しています
half spherical_16(float3 nor)
{
float2 v = float2(0.5 + 0.5 * atan2(nor.z, nor.x) / 3.141593, acos(nor.y) / 3.141593);
uint2 d = uint2(round(v * 255.0));
return d.x | (d.y << 8u);
}
球面座標は(r, theta, phi)で表される極座標系ですが、法線は単位ベクトルなので r(ベクトルの長さ)を記録する必要がありません
今回はxz平面のx軸から法線までの角度をphi、 y軸と法線間の角度をthetaとします
phi −π < φ ≤ π
theta 0 ≤ θ ≤ π
です
atan2(nor.z, nor.x)
と acos(nor.y)
で各角度を求めることができます
float2 v = float2(0.5 + 0.5 * atan2(nor.z, nor.x) / 3.141593, acos(nor.y) / 3.141593);
uint2 d = uint2(round(v * 255.0));
上記の処理で 0 ≤ x ≤ 255
の範囲にします
return d.x | (d.y << 8u);
phiと8bit左シフトしたthetaの論理和を取ってhalfに格納します
解凍
float3 i_spherical_16(half data)
{
uint d = data;
float2 v = float2(d & 255u, d >> 8) / 255.0;
v.x = 2.0 * v.x - 1.0;
v *= 3.141593;
return normalize(float3(sin(v.y) * cos(v.x), cos(v.y), sin(v.y) * sin(v.x)));
}
圧縮部分の処理が理解出来ていれば難しくはありません
phiとthetaを取り出して元の値に戻した後、球面座標から直交座標に変換しています
参考