在讀這篇博客的時候,需要讀者具備一些基本的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)
此時得到的是綠色的向量。
那麼我們知道了,其實上面的這個採樣的過程就是,根絕法線,求得一個局部的座標系,然後根據半球上的某個向量,
在這個局部座標系下的座標是多少。
半球的法線變了,那麼這個採樣向量的方向也會跟着變,但是始終是在正半球的上方。這樣就可以進行預積分了。