PBR之基于图像的光照IBL (Diffuse)

基于图像的光照(Image Based Lighting)

在开始之前,我们先来了解下什么是基于图像的光照(IBL)。一个物体,不会单独的存在一个空空的环境里面,它的周围一定有其他的物体。当光源照射到其他物体上的时候,一定也会反射,其中就有很多反射的光线会反射到该物体上去。上一篇文章中我们模拟的是直接光照。对于直接光照系统,像上面那种其他物体反射过来的光,我们一般就只是使用一个Ambient项来模拟。这种模拟方法只能够模拟单调的环境光照效果,想要更加丰富,更加精细的效果,我们就需要使用更加丰富的环境光照系统,而IBL就是实现它的一种方式。

一般来说,我们通过一张环境贴图(Environment Map)来保存一个物体周围的环境信息,然后通过某种处理,来实现丰富的环境光照效果。本文就是讲述,如何通过对环境贴图进行处理,然后实现丰富的环境光照效果。

从渲染方程解释IBL

根据前面对环境光照的描述,环境光照也应该符合这个公式,只不过相对于直接光照,它需要计算更多的入射光线。

L o ( p , ω o ) = Ω ( k d c π + k s D F G 4 ( ω o n ) ( ω i n ) ) L i ( p , ω i ) n ω i d ω i L_o(p,\omega_o) = \int\limits_{\Omega} (k_d\frac{c}{\pi} + k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i

同时从渲染方程可以看出,我们可以把渲染方程拆成两个部分进行处理:

L o ( p , ω o ) = Ω ( k d c π ) L i ( p , ω i ) n ω i d ω i + Ω ( k s D F G 4 ( ω o n ) ( ω i n ) ) L i ( p , ω i ) n ω i d ω i L_o(p,\omega_o) = \int\limits_{\Omega} (k_d\frac{c}{\pi}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i + \int\limits_{\Omega} (k_s\frac{DFG}{4(\omega_o \cdot n)(\omega_i \cdot n)}) L_i(p,\omega_i) n \cdot \omega_i d\omega_i

本篇文章集中于处理:

L o ( p , ω o ) = k d c π Ω L i ( p , ω i ) n ω i d ω i L_o(p,\omega_o) = k_d\frac{c}{\pi} \int\limits_{\Omega} L_i(p,\omega_i) n \cdot \omega_i d\omega_i

对于这个方程,我们就可以将周围环境的所有光照信息保存在一张环境贴图中,而这个环境贴图就模拟了所有的。

环境贴图

在图形领域,用于保存周围环境信息的环境贴图有多种形式,如

现在业界,对于IBL普遍使用的是Cube Map的形式。本篇文章也将主要使用Cube Map来进行IBL。

HDR对于PBR非常重要,没有了HDR,PBR的效果将大大折扣。所以,对于IBL来说,我们依然需要使用HDR。也就说,对于周围环境光照的描述,需要通过HDR的格式文件来保存。

本文的所有使用的环境光照贴图将从sIBL中获取,这个网站里面有很多免费使用的HDR光照贴图,我们将从这些图中选择一些进行测试。

在这里插入图片描述

需要注意的是,这个网站里面的HDR贴图并不是CubeMap的形式,而是EquirectangularMap的形式进行保存的,所以接下来我们需要解决两个问题:如何读取.hdr文件,如何对这个贴图进行filter。

.hdr文件读取

在sIBL网站上,已经给出了.hdr文件格式的详细描述。我这里为了方便就直接使用了github上开源的stb_image库来读取.hdr文件。这个库里面都是一些单个文件的c代码库,感兴趣的读者可以自行探索。

得到.hdr文件保存的HDR数据了,然后可以通过图形API创建一个2D的HDR纹理

Equirectangular Map Filter

我们前面说过,我们将使用Cube Map来进行IBL。所以,我们需要一种方法来将该Equirectangular Map转换为Cube Map。为此,我们先简单的绘制一个球体,然后将这个Equirectangular Map贴上去,然后使用传统的创建Cube Map的方式产生一张Cube Map。

#version 330 core
out vec4 FragColor;
in vec3 WorldPos;

uniform sampler2D equirectangularMap;

const vec2 invAtan = vec2(0.1591, 0.3183);
vec2 SampleSphericalMap(vec3 v)
{
    vec2 uv = vec2(atan(v.z, v.x), asin(v.y));
    uv *= invAtan;
    uv += 0.5;
    return uv;
}

void main()
{		
    vec2 uv = SampleSphericalMap(normalize(WorldPos));
    vec3 color = texture(equirectangularMap, uv).rgb;
    
    FragColor = vec4(color, 1.0);
}

通过计算atan(n.z, n.x)就能够得到具有该法线顶点的UV坐标的U值,通过计算asin(n.y)就能够得到具有该法线顶点的UV坐标的V值。同时,由于atan函数返回的结果在[−π,π]
之间,而asin返回的结果在[−π/2,π/2]之间,所有需要把它们都映射到[0,1]之间。

在得到了这个球体之后,我们就可以简单的使用传统的方法来创建CubeMap,主要就是通过设置FOV为90度的摄像机,分别朝着+X,-X,+Y,-Y,+Z,-Z去观察该球体,然后渲染CubeMap的6个面,从而得到一张HDR的CubeMap。

预计算辐射光照贴图

在这里插入图片描述

为了方便进行积分运算,一般都将渲染方程改为球面坐标的积分形式,其中:

n ω i = cos θ n⋅\omega_i=\cos\theta

d ω i = sin θ d ϕ d θ d\omega_i=\sin\theta d\phi d\theta

所以,方程转变为如下形式:

(1) L o ( p , ϕ o , θ o ) = k d c π ϕ = 0 2 π θ = 0 1 2 π L i ( p , ϕ i , θ i ) cos θ sin θ d ϕ d θ L_o(p,\phi_o, \theta_o) = k_d\frac{c}{\pi} \int_{\phi = 0}^{2\pi} \int_{\theta = 0}^{\frac{1}{2}\pi} L_i(p,\phi_i, \theta_i) \cos\theta \sin\theta d\phi d\theta \tag{1}

上述公式转换为Riemann Sum(黎曼和)的表述:

Riemann Sum是一种很简单的积分方法,当我们的步进值越小的时候,通过这种方法计算出来的h值就越加的接近真实值。

(2) L o ( p , ϕ o , θ o ) = k d c π 1 n 1 n 2 ϕ = 0 n 1 θ = 0 n 2 L i ( p , ϕ i , θ i ) cos ( θ ) sin ( θ ) d ϕ d θ L_o(p,\phi_o, \theta_o) = k_d\frac{c}{\pi} \frac{1}{n1 n2} \sum_{\phi = 0}^{n1} \sum_{\theta = 0}^{n2} L_i(p,\phi_i, \theta_i) \cos(\theta) \sin(\theta) d\phi d\theta \tag{2}

vec3 irradiance = vec3(0.0);  

vec3 up    = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, normal);
up         = cross(normal, right);

float sampleDelta = 0.025;
float nrSamples = 0.0; 
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
    for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
    {
        // spherical to cartesian (in tangent space)
        vec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));
        // tangent space to world
        vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; 

        irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
        nrSamples++;
    }
}
irradiance = PI * irradiance * (1.0 / float(nrSamples));

预计算辐射光照贴图(irradianceMap). 这个计算过程输入为一张环境贴图 CubeMap .输出也是一张 CubeMap. 如下图所示.

在这里插入图片描述

在球谐面卷积采样, 生成的图像有点类似模糊的效果。离线生成的irradianceMap大小就32x32, 精度不需要那么高, 中间值使用插值就可以得到。

整个流程:

equirectangularmap(.hdr) -> envCubemap -> irradianceMap

在引擎里看到的效果,就是如下所示:

在这里插入图片描述

发布了27 篇原创文章 · 获赞 7 · 访问量 7308

猜你喜欢

转载自blog.csdn.net/yunman2012/article/details/94404062