球谐函数在图形学里最大的应用就是编码压缩irradiance map。
从radiance map输出irradiance map
radiance map是什么
wiki:https://en.wikipedia.org/wiki/Reflection_mapping
其实也就是hdr map、enviroment map。一般是一张全景照片。
可想而知,这张图图片一般可以用cube map的方式存储,即6张方形图,这其实就是skybox的贴图了(天光)。
为什么又叫hdr map,是因为为了得到pbr效果的话,这张radiance map的亮度范围要允许足够大,如果只是存0-255的亮度的话就不是pbr了,hdr map至少是32位float。
球形环境贴图
6张方形图虽然好理解,但是不利于计算。于是就有了球形环境贴图的存储方式,球形环境贴图一般宽度是高度的两倍,往中间切开的话,能得到2张1:1的方形图。
球形环境贴图的坐标理解
因为像素有面积,那么可设:
- 每个pixel左下角的坐标为(x,y),则(x=0,y=0)代表图像左下的第一个像素,(x=w-1,y=h-1)代表右上的最后一个像素
- 用(u,v)表示像素的单位化坐标,那么( u=(x+0.5)/w,v=(y+0.5)/h )就表示了像素(x,y)的中心点,( u=0,v=0 )代表图像左下的第一个像素的左下角,而( u=(w-1+1)/w,v=(h-1 + 1)/h ),即(u=1,v=1) 表示右上的最后一个像素的右上角
可见uv的取值范围是[0, 1]。我们此时已得到了像素坐标xy转成uv坐标的方法。
再设球形环境贴图的横坐标轴为θ,纵坐标是φ(0到π),
我们想要的θ、φ取值需要在指定范围,θ是-π到π,φ是0到π,θ相当于环绕y轴一整个圆圈,φ则相当于绕x轴半个圆圈。这样就覆盖了整个单位球的球面(记住这是3D空间的球)。
θ是-π到π的好处是,能使得球形环境贴图是看起来是对称的。左半边的横坐标轴取值范围是-π到0,右半边的范围是0到π,纵轴一样是0到π。
可以用以下公式把uv坐标转成θ、φ球坐标:
float theta = pi*(uv.x*2.0f - 1.0f);
float phi = pi*uv.y;
有了θ、φ球坐标后,还可以转成3D笛卡尔坐标。
\[ x = r * cos\theta sin \phi \]
\[ y = r * sin\theta sin \phi \]
\[ z = r * cos \phi \]
float x = sin(phi)*sin(theta);
float y = cos(phi);
float z = -sin(phi)*cos(theta); // 这里故意对z取反,不影响变换正确性
x、y、z做了一些调换。
此时我们就得到了从像素坐标到3D笛卡尔坐标的转换公式。在CPU计算irradiance map时,可以预先把这个转换关系烘焙到一个w*h大小的、元素为vector3的二维数组里,称之为directionImage。下文会用到这个directionImage。
什么是irradiance map
前面介绍了radiance map是一张场景全景图,那么当场景里有一个对象,要计算场景对该对象的光照影响时,并不是直接在这个对象的fragment shader里去采样radiance map,这是错误的。
首先假设只考虑diffuse光照的情况。那么对象上的每一个着色点,都受到场景四面八方的环境光影响。所以每一个着色点应该是去采样整张环境贴图(假设没有遮挡),而不是环境贴图的一个点。
用代码解释会更好理解,但需要回顾下什么是蒙特卡洛算法。因为采样整张环境贴图就是一次积分过程,于是可以用蒙特卡洛,把积分变成累加再求平均,以下是没有重要性采样的蒙特卡罗算irradiance map的算法:
// m_directionImage包含了所有方向,direction是笛卡尔坐标系朝向,pixelPos是2D像素坐标
data.m_directionImage.parallelForPixels2D([&](const vec3& direction, ivec2 pixelPos)
{
// basis是以该方向为法线的世界空间正交坐标系
mat3 basis = makeOrthogonalBasis(direction);
vec3 accum = vec3(0.0f);
// 迭代随机采样1000次,随机一个方向
for (u32 sampleIt = 0; sampleIt < m_hemisphereSampleCount; ++sampleIt)
{
// u等于sampleIt/m_hemisphereSampleCount,v随机
vec2 sampleUv = sampleHammersley(sampleIt, m_hemisphereSampleCount);
// uv转成球坐标再转成笛卡尔坐标,注意这是local坐标系的
vec3 hemisphereDirection = sampleCosineHemisphere(sampleUv);
// 投影到该世界坐标系
vec3 sampleDirection = basis * hemisphereDirection;
// 采样hdr map得到该方向对应的radiance只,加到accum
accum += (vec3)m_radianceImage.sampleNearest(cartesianToLatLongTexcoord(sampleDirection));
}
// 求平均
accum /= m_hemisphereSampleCount;
// 输出到irradiance map
m_irradianceImage.at(pixelPos) = vec4(accum, 1.0f);
});
其中m_irradianceImage是一个和m_radianceImage一样大的数组。
有重要性采样的蒙特卡洛,同样采样数的前提下,精度会更高。因为涉及到复杂的pdf问题,这里不介绍了。
用球谐函数编码irradiance map
上一节的蒙特卡洛方法求出来的irradianceImage虽然非常准确,但是很占内存。这对于实时渲染是很糟糕的,意味着要让gpu缓存一张巨大的irradiance map贴图。
记采样方向为θ、φ,以及f(θ, φ)表示该方向的环境光,因为f本质是一个积分,那么
球谐的基函数
生成步骤
写作不易,您的支持是我写作的动力!