在 Raymarching 第一篇文章的时候,介绍了基本的 Phong光照模型,同时实现了漫反射和环境光照。本文将实现一些更加真实的光照效果,并根据物理自然现象解释其数学原理,并完成代码编写,Let's Go
漫反射扩散光 Diffuse Lighting
Lambertian光照模型是一种计算机图形学中常用的光照模型,以简洁的数学形式描述了漫反射(Diffuse Reflection)的效果。它基于光的入射角度和表面法线之间的关系来计算光的漫反射强度。它假设反射表面是完全漫反射的,即表面均匀地散射入射光线,没有任何方向依赖性。这种漫反射表面也称为"朗伯表面"。主要概念有
- 法向量(Normal Vector, n):垂直于表面的一条单位向量,表示该表面的朝向。
- 入射光方向(Light Direction, L):从表面指向光源的方向向量。需要标准化为单位向量。
- 漫反射系数(Diffuse Coefficient, kd):表示表面的漫反射能力,取值范围为0到1,0表示完全不反射,1表示完全反射。 其核心公式
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> I d = I l ⋅ K d ⋅ m a x ( c o s ( θ ) , 0 ) I_d = I_l \cdot K_d \cdot max(cos(\theta), 0) </math>Id=Il⋅Kd⋅max(cos(θ),0)
其中
- Id 为 是漫反射光的强度。
- Il 为 入射光的强度
- Kd 为漫反射系数。
- θ 是光线方向和法向量之间的夹角。 当然看到cos就想到了点积运算, 所以最后代码
Lambertian光照优化计算
在 shader中计算曲面的法向量,需要进行 6 次计算,在raymarching第一节已经讲过了,IQ提出了一种基于方向导数的近似方法,核心思想是沿着光的方向做一次求导就可以近似得到 符合 Lambertian光照模型的结果. 详细可查看这篇文章 iquilezles.org/articles/de... 关键公式是
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Δ v f ( x ) = Δ f ( x ) ⋅ v ∣ v ∣ \Delta_v f(x) = \Delta f(x) \cdot \frac{v}{|v|} </math>Δvf(x)=Δf(x)⋅∣v∣v
求方向导数可以用以下代码实现,eps为微分,取值越小精度越高
glsl
float diffuse = (map(pos+eps*light)-map(pos) ) / eps
镜面反射高光 Specular Lighting
Phong光照模型将光照效果分为三个主要部分:
- 环境光(Ambient Light):模拟环境中的散射光,通常是一个常量值。
- 漫反射光(Diffuse Light):基于表面法线和光线方向的点积,描述光线在表面上的散射效果。
- 镜面反射光(Specular Light):描述光线在表面上的镜面反射,通常产生高光效果。 其中镜面反射的核心公式为
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> I s = I l ⋅ K s ⋅ m a x ( c o s ( α ) , 0 ) n I_s = I_l \cdot K_s \cdot max(cos(\alpha), 0)^n </math>Is=Il⋅Ks⋅max(cos(α),0)n
- Is 为 是镜面反射光的强度。
- Il 为 入射光的强度
- Ks 为漫反射系数。
- α 是反射光与视线方向之间的夹角
- n 是镜面高光的锐度(反射率),通常称为 "高光指数"
glsl
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), shininess);
vec3 specular = ks * spec * lightColor;
Blinn-Phong优化
Blinn-Phong 反射模型是基于Phong反射模型的一种改进,用于模拟物体表面的光照效果。它在计算镜面反射光(Specular Reflection)时引入了一个半向量(Half-Vector)的概念,使得反射高光的计算更加高效且效果更自然。其中镜面反射的核心公式为
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> I s = I l ⋅ K s ⋅ m a x ( c o s ( β ) , 0 ) n I_s = I_l \cdot K_s \cdot max(cos(\beta), 0)^n </math>Is=Il⋅Ks⋅max(cos(β),0)n
Half-vector是光线方向向量 𝐿与视线向量 𝑉 的平均向量
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> H = L + V ∣ ∣ L + V ∣ ∣ H = \frac{L+V}{||L+V||} </math>H=∣∣L+V∣∣L+V
β 是法向量 𝑁 与半向量 𝐻之间的夹角。
Fresnel 方程与近似计算
Fresnel 方程描述了光在不同介质界面上反射和折射时的行为,提供了反射率和折射率随入射角变化的详细计算方法。广泛应用于光学、计算机图形学和渲染领域,以模拟光线与物体表面交互时的反射和折射现象。在实际的Shader中,Fresnel 的效果经常用于模拟反射率随视角的变化,以生成更逼真的光学现象。例如,水面或者眼睛等表面材料在不同的观察方向上会有不同的反射强度。 例如上图中,远处更多的是看冰山的反射,而近处可以看到水面
当光线从一种介质(折射率为 n1)进入另一种介质(折射率为 𝑛2)时,光被界面反射的比例称为反射比𝑅,完整的 Fresnel 方程计算比较复杂,Schlick 提出了一个简化的近似公式,以便计算中更为高效。Schlick's 近似公式如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> R ( θ ) = R 0 + ( 1 − R 0 ) ( 1 − c o s θ ) 5 R(\theta) = R_0 + (1 - R_0)(1-cos\theta)^5 </math>R(θ)=R0+(1−R0)(1−cosθ)5
R0是在法线方向的反射率,可以通过
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> R 0 = ( n 1 − n 2 n 1 + n 2 ) 2 R_0 = (\frac{n_1-n_2}{n_1+n_2})^2 </math>R0=(n1+n2n1−n2)2
θ 是入射光与法线之间的夹角。写成伪代码有
glsl
float fresnelSchlick(float cosTheta, float r0)
{
return r0 + (1.0 - r0) * pow(1.0 - cosTheta, 5.0);
}
环境光 Ambient Light
正常代码中环境光是全局都是一样,但实际上随着高度越高,空气越好环境光损失的越小,所以在这里增加一个 Y方向的 调整。同时sqrt 平滑光线分布。
glsl
float sky_dif = sqrt(clamp(0.5 + 0.5 * nor.y, 0.0, 1.0));
地面反射光 (Bounce Light)
空旷地面也会反射大自然的光线,这个光线的强度取决于物体离地面的距离,越近越强。同时也取决物体表面的朝向,如果完全朝上也就是 Y分量特别大,地面的反射光就会被遮蔽。 所以 IQ创造了这样的函数, 同样也用sqrt平滑法向量光线分布
glsl
float bou_dif = sqrt(clamp(0.1 - 0.9 * nor.y, 0.0, 1.0)) *
clamp(1.0 - 0.1 * pos.y, 0.0, 1.0);
代码实现
glsl
void calcLight(
inout vec3 col,
float dist,
vec3 rayDirection,
vec3 sunLightDirection,
vec3 position,
float time
) {
vec3 surfaceNormal = calcNormal( position, time );
float kSpecular = 1.0;
float sunDiffuse = clamp(dot( surfaceNormal, sunLightDirection ), 0.0, 1.0 );
vec3 halfVector = normalize( sunLightDirection-rayDirection );
float isBocked = castRay(position+SURFACE_DIST*surfaceNormal, sunLightDirection,time).y;
float sunShadow = step(isBocked,0.0);
float blinnSpecular = kSpecular*pow(clamp(dot(surfaceNormal,halfVector),0.0,1.0),8.0);
float fresnelSchlick = 0.04+0.96*pow(clamp(1.0+dot(halfVector,rayDirection),0.0,1.0),5.0);
float sumSpecular = blinnSpecular * fresnelSchlick * sunDiffuse;
float skyDiffuse = sqrt(clamp( 0.5+0.5*surfaceNormal.y, 0.0, 1.0 ));
float faceToLand = sqrt(clamp( 0.1-0.9*surfaceNormal.y, 0.0, 1.0 ));
float nearToLand = clamp(1.0-0.1*position.y,0.0,1.0);
float bounceDiffuse = faceToLand * nearToLand;
vec3 light = vec3(0.0);
light += skyDiffuse*vec3(0.50,0.70,1.00); // sky is blue 蓝天
light += bounceDiffuse*vec3(0.40,1.00,0.40); // land is green 绿地
light += sunDiffuse*vec3(8.10,6.00,4.20)*sunShadow;// sun is red 红日
col = col*light;
col += sumSpecular*vec3(8.10,6.00,4.20)*sunShadow;
col = mix( col, vec3(0.5,0.7,0.9), 1.0-exp( -0.0001*dist*dist*dist ) );
}
最后我们得到这么美丽的空间,🎉🎉🎉🎉🎉🎉