OpenGL高级版本学习日志2:光照模型&材质

 

1. 前言

促使我学习OpenGL新版本的一大动力,就是对改善渲染质量的渴望。谈到渲染,就不能不提光照模型。好的光照模型,对物体的真实感渲染起到至关重要的作用。本章我将从光照模型这个主题切入,来谈一谈如何通过编写shader来控制光照。

注:本章的部分内容为主观叙述,未经严格的认证,如有问题,请以OpenGL新版教材为准。关于新版本的一些基础知识,如VBO,VAO的绑定,管线编程机制等,在本文中不做具体介绍。

2. 光照模型

作为一个经典的光照模型,冯氏光照模型(Phong Lighting Model)被广泛使用,其主要结构包含三个部分。环境光照,漫反射光照和镜面光照。

环境光照通常在没有特殊光源的情况下,我们在白天仍然能够看清室内室外的物体,其原因就是因为存在环境光。环境光主要指的就是太阳光。太阳光在通过多次反射与衍射,形成了一个近似均匀的光场。基于这样一个光场,物体才能够被看到。在没有太阳光照的晚上,没有特殊光源的辅助,自然就看不到物体了。在图形渲染引擎中,为了能够看到物体,通常会默认加入这样一个环境光作为基础,以模拟太阳光产生的均匀光场。

漫反射光照:在环境光的基础上,如果有特殊的光源,自然会对物体的光照渲染有不同的影响。比如当有一盏台灯的地方,靠近台灯,且面向台灯的一面,自然会接收到更强的光照,在渲染时产生更加明亮的效果。由特别光源驱动,并更具物体的不同位置与接受光源的不同角度产生的光照效果,即为漫反射光照。通常意义上,漫反射光照是所有光照最明显,对物体光照渲染影响最大的一个。

镜面光照:俗称高光。当我们的观察视角和光源反射到观察视角的角度约接近,越能够看到物体的反射区域呈现出一块比其他区域明亮的地方,这个明亮的区域就是高光,即镜面光照。如果你对素描有一定的基础,则对高光的理解会更加深入。高光通常反映了观察视角和渲染物体本身反射光照的位置关系以及渲染物体表面本身的材质。

将以上三种光照叠加,就组成了冯氏光照模型。

3. 利用Shader实现光照模型

这里需要分为两个部分。一个部分是光源的,一个部分是对物体的。

光源比较好理解,这里就是一个点光源,其颜色与亮度是恒定的。它的顶点着色器为:

#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
	gl_Position = projection * view * model * vec4(aPos, 1.0);
}

就是一个最基本的视图变换。其片段着色器同样简单,就是一个单一颜色的光源。

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0); // set alle 4 vector values to 1.0
}

相对比较复杂的是被渲染物体,其顶点着色器为:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;  
    
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

除了基本的视图坐标变换以外,我们发现还有两个属性,即FragPos和Normal,对应世界坐标和法向。这两个属性对于光照渲染十分重要,因此需要在顶点坐标变换的时候进行输出。世界坐标定义了物体与光源的距离与入射角度,法向定义了物体与光源的角度。有这两个属性,就能够决定在片段着色器中如何实现光照渲染。以下就是光照渲染中最核心的物体片段着色器代码:

#version 330 core
out vec4 FragColor;

in vec3 Normal;  
in vec3 FragPos;  
  
uniform vec3 lightPos; 
uniform vec3 viewPos; 
uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
    // ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
  	
    // diffuse 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // specular
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64);
    vec3 specular = specularStrength * spec * lightColor;  
        
    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
} 

由于这段代码对于理解光照渲染十分重要,我们逐句进行解释:

1)float ambientStrength = 0.1; 
      vec3 ambient = ambientStrength * lightColor;

最容易理解的就是环境光。输入基础光照和定义一个基础强度,就定义了环境光。

2)vec3 norm = normalize(Normal);
     vec3 lightDir = normalize(lightPos - FragPos);
     float diff = max(dot(norm, lightDir), 0.0);
     vec3 diffuse = diff * lightColor;

首先单位化法向,方便计算。之后计算入射光线法向,同时也进行单位化。注意,在计算入射光线角度的时候,用到了世界坐标,这就是为什么需要世界坐标的原因了。接下来就是根据物体一点的法向与入射光线角度的点积,进而得到漫反射强度。当两个向量       的夹角为90度时,漫反射强度为0,可以想象成一个二维面片,用它的边正对光源,使之面向光源的面积无限趋近于0,即没有任何反射。当超过90度时,即将被光面面向光源,为了防止渲染错误,顾设置最小值为0。

3)float specularStrength = 0.5;
      vec3 viewDir = normalize(viewPos - FragPos);
      vec3 reflectDir = reflect(-lightDir, norm);  
      float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64);
      vec3 specular = specularStrength * spec * lightColor;  

镜面反射主要涉及到观察角度与光源反射角度的关系。首先获得观察方向,即摄像机与物体位置生成的向量viewDir。反射角度也好理解,就是以法线为准,以入射光线为输入,输出一个反射光线的向量R,计算R与方向的角度Θ,即内积。使用pow以及次数来控制反射强度,因为max(dot(viewDir, reflectDir), 0.0)最大值为1,所以使用的次数越大,相当于强度约弱。结合基础强度加夹角强度以及光照颜色,这样就得到了镜面光照的结果。

   

4)vec3 result = (ambient + diffuse + specular) * objectColor;
     FragColor = vec4(result, 1.0);

最后将三个光照加在一起,并叠加上物体本身的颜色,得到冯氏光照模型的最后结果。

完整代码链接:https://learnopengl.com/code_viewer_gh.php?code=src/2.lighting/2.2.basic_lighting_specular/basic_lighting_specular.cpp

4. 材质

如果你了解了整个的光照模型,你会发现可以通过一些参数来改变光照的效果,如法线,光照强度,反射强度等等。这些参数在现实中反映了物体表面的一些物理性质。我们将这些数据进行打包,就能够得到具有特定物理性质的渲染结果。这种对物理性质模拟的打包,可以被成为材质,如下所示:

#version 330 core
struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
}; 

uniform Material material;

可以看到shader用一个结构体来打包了光照模型的基本数据,片段着色器也就被改为:

void main()
{    
    // 环境光
    vec3 ambient = lightColor * material.ambient;

    // 漫反射 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = lightColor * (diff * material.diffuse);

    // 镜面光
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    vec3 specular = lightColor * (spec * material.specular);  

    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 1.0);
}

可以看到,材质的参数在各个光照分量中都施加偶尔自己的影响。通过对这些参数进行特定的组合,能够得到对不同材质的光照模拟:

猜你喜欢

转载自blog.csdn.net/aliexken/article/details/110807152