在读这篇博客的时候,需要读者具备一些基本的IBL(image-based lighting)的相关知识。
比如什么是辐射率,辐射度,立体角等相关的知识。还需要知道球坐标和笛卡尔坐标之间的相互转的关系等。
同时官方参考网址:https://learnopengl.com/PBR/IBL/Diffuse-irradiance
源代码也有:https://github.com/JoeyDeVries/LearnOpenGL.git
对应的源码是:
ok,完整的代码,读者都可以自行下载进行分析。
下面是我再次读后的读后感。
1、如果场景中只有有限个光源(无论是点光源还是平行光),那么其中计算某个光源对物体表面一点的影响是直观的而且是简单的,只要用兰伯特n和l直接点乘,再做对应的衰减等,就可以完成光照的计算。
这些都是直接的光照。
优点是计算简单,直观;缺点是没有考虑周围环节的影响(再具体地说就是环境贴图对物体的影响)。
2、如果把周围的环境(环境或者天空盒)都考虑进去的话,那么意味着什么,意味着,环境贴图上的每个像素都是一个点光源,都会对物体上的一个点,进行光照影响,这就是IBL的核心。
那么怎么将这个算法落地实现呢?
答案就是对环境贴图进行预处理。这里我们要明确算法的流程:
- 以物体上的一个点p为例:
它需要考虑所有方向进入的光线对其的影响,所有就要进行积分运算。
那么积分又是不切实际的,对于实时光照来说,所以考虑是否能将环境贴图进行预处理,最后只要给出一个方向,直接采样就行。
答案是肯定的,这也是本节主要的讲解的内容。
- 预处理环境贴图的shader编写
Shader irradianceShader("2.1.2.cubemap.vs", "2.1.2.irradiance_convolution.fs");
顶点着色器是:
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 WorldPos;
uniform mat4 projection;
uniform mat4 view;
void main()
{
WorldPos = aPos;
gl_Position = projection * view * vec4(WorldPos, 1.0);
}
顶点着色器的输入只有一个:aPos
输出有两个:WorldPos和gl_Position
下面看下片元着色器:
#version 330 core
out vec4 FragColor;
in vec3 WorldPos;
uniform samplerCube environmentMap;
const float PI = 3.14159265359;
void main()
{
// The world vector acts as the normal of a tangent surface
// from the origin, aligned to WorldPos. Given this normal, calculate all
// incoming radiance of the environment. The result of this radiance
// is the radiance of light coming from -Normal direction, which is what
// we use in the PBR shader to sample irradiance.
vec3 N = normalize(WorldPos);
vec3 irradiance = vec3(0.0);
// tangent space calculation from origin point
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, N);
up = cross(N, right);
直接将WorldPos作为切空间的法线方向。
切空间的up向量这里定义为:(0.0,1.0,0.0)
opengl是右手坐标系,计算right是up叉乘N,得到了right向量。
而up向量,又用N叉乘right得到。
这样up、right、N分别正交,构成了切空间。
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));
FragColor = vec4(irradiance, 1.0);
}
第一个for:增量是sampleDelta=0.025,范围是0到2PI(方位角)
第二个for:增量是sampleDelta=0.025,范围是0到0.5PI(天顶角)
然后是球坐标转换为笛卡尔坐标:
x=r*sinθ*cosφ
y=r*sinθ*sinφ
z=r*cosθ
笛卡尔坐标系(x,y,z)与球坐标系(r,θ,φ)的转换关系
我们可以在unity画下试试:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
public Vector3 normal;
public float length = 4;
public float sampleDelta = 1.0f;
private const float PI = 3.14159265359f;
public void OnDrawGizmos()
{
Vector3 N = Vector3.Normalize(normal);
Vector3 up = new Vector3(0.0f, 1.0f, 0.0f);
Vector3 right = Vector3.Cross(up, N);
up = Vector3.Cross(N, right);
Gizmos.color = Color.blue;
Gizmos.DrawLine(Vector3.zero, N.normalized * 2 * length);
Gizmos.color = Color.red;
Gizmos.DrawLine(Vector3.zero, right.normalized * 2 * length);
Gizmos.color = Color.green;
Gizmos.DrawLine(Vector3.zero, up.normalized * 2 * length);
Gizmos.color = Color.white;
for (float phi = 0.0f; phi < 2.0 * PI; phi += sampleDelta)
{
for (float theta = 0.0f; theta < 0.5 * PI; theta += sampleDelta)
{
Vector3 tangentSample = new Vector3(
Mathf.Sin(theta) * Mathf.Cos(phi),
Mathf.Sin(theta) * Mathf.Sin(phi),
Mathf.Cos(theta));
Vector3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N;
Gizmos.DrawLine(Vector3.zero, sampleVec.normalized * length);
}
}
}
}
这其实不是什么坐标转换,如果换个思路理解就更简单了。
看下图:
如图所示向量(1,1)。它实在x轴(1,0)和y轴(0,1)这个坐标系的坐标为(1,1)。
试想着,如果我们把坐标系旋转个45度,那么此时x’轴就为(1,-1)而y’轴为(1,1)。
那么此时向量(1,1)在这个坐标系下的坐标是多少呢?
很自然的点乘得到:
x轴坐标为:(1,1)dot(1,-1)=0
y轴坐标为:(1,1)dot(1,1)=2
单位化之后得到:(0,1)
此时即可为:(0,2)如下图:
所以这才是空间的变换。变换之后向量的实际位置是不变的。
那么回到上面的问题。如果我们使用原方法:
Vector3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N;
求(1,1)的向量那么得到什么呢?
1*(1,-1)+1*(1,1)=(2,0)
此时得到的是绿色的向量。
那么我们知道了,其实上面的这个采样的过程就是,根绝法线,求得一个局部的坐标系,然后根据半球上的某个向量,
在这个局部坐标系下的坐标是多少。
半球的法线变了,那么这个采样向量的方向也会跟着变,但是始终是在正半球的上方。这样就可以进行预积分了。