Shader 3d RayMarching8 光照

在 Raymarching 第一篇文章的时候,介绍了基本的 Phong光照模型,同时实现了漫反射和环境光照。本文将实现一些更加真实的光照效果,并根据物理自然现象解释其数学原理,并完成代码编写,Let's Go

漫反射扩散光 Diffuse Lighting

Lambertian光照模型是一种计算机图形学中常用的光照模型,以简洁的数学形式描述了漫反射(Diffuse Reflection)的效果。它基于光的入射角度和表面法线之间的关系来计算光的漫反射强度。它假设反射表面是完全漫反射的,即表面均匀地散射入射光线,没有任何方向依赖性。这种漫反射表面也称为"朗伯表面"。主要概念有

  1. 法向量(Normal Vector, n):垂直于表面的一条单位向量,表示该表面的朝向。
  2. 入射光方向(Light Direction, L):从表面指向光源的方向向量。需要标准化为单位向量。
  3. 漫反射系数(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光照模型将光照效果分为三个主要部分:

  1. 环境光(Ambient Light):模拟环境中的散射光,通常是一个常量值。
  2. 漫反射光(Diffuse Light):基于表面法线和光线方向的点积,描述光线在表面上的散射效果。
  3. 镜面反射光(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 ) );

}

最后我们得到这么美丽的空间,🎉🎉🎉🎉🎉🎉

www.shadertoy.com/view/McfcR7

相关推荐
小彭努力中2 天前
20. gui调试3-下拉菜单、单选框
前端·3d·webgl
还是大剑师兰特2 天前
webGL 综合教程100+【目录】
webgl·webgl教程·webgl 示例
xiangshangdemayi5 天前
WebGL系列教程八(GLSL着色器基础语法)
webgl·基础·shader·着色器·语法·glsl
wjs04065 天前
WebGL入门:将3D世界带入网页的魔法
javascript·3d·webgl·前端开发
xiangshangdemayi5 天前
WebGL系列教程六(纹理映射与立方体贴图)
webgl·贴图·uv·立方体·纹理坐标·纹理映射
refineiks8 天前
three.js使用3DTilesRendererJS加载3d tiles数据
前端·3d·图形渲染·webgl
Ian102510 天前
Three.js new THREE.TextureLoader()纹理贴图使用png图片显示为黑色
前端·javascript·webgl·three.js·贴图·三维
常城12 天前
解决TMP_InputField 在WebGL(抖音)上不能唤起虚拟键盘,不能使用手机内置输入法的问题
webgl
DSLMing14 天前
微信小程序webgl 显示图片
微信小程序·小程序·webgl
GISer_Jing14 天前
Cesium加载高速公路样式线图层和利用CSS撰写高速公路样式
前端·css·webgl