【OpenGL学习】OpenGL实现 基于Phong模型的基础光照

基于Phong模型的基础光照

在本节中,我们将利用 Phong 光照模型来完成一个简单的光照场景的渲染。

一、Phong 光照模型

Phong光照模型是20世纪70年代被提出的一种渲染逼真图像的方法,模型的提出者是越南出生的计算机图形学研究员Bui Tuong Phong(1942-1975),他提出的近似方法实现了较逼真的图像。

Phong 模型是一种基于经验的光照模型,因此渲染出的图像一般不够真实,但是该模型计算消耗的性能较低,非常适合在一些对真实感要求不太高的场景中使用。

Phong 模型的计算主要由三个分量组成: 环境光项(Ambient)、漫反射项(Diffuse)以及高光反射项(Specular)。三个分量对应的光照结果如下图所示:

img

  • 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
  • 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
  • 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。

二、添加环境光照

环境光产生的原因一般是某种光源照亮了某个物体,这个物体将光线反射到我们观察的物体上面,使我们能够看到间接的光照,考虑到这种情况,计算就会变得非常复杂,这也是==全局光照(Global Illumination)==算法中研究的内容,Phong 模型允许我们对该部分的照明进行简化,也就是我们所说的环境光照,原理是将一个很小的颜色分量加到物体的每个片元着色计算当中,来模拟间接光照的影响。

在箱子的片元着色器中,添加一个环境光颜色:

void main()
{
	float ambient_strength = 0.1;
	vec3 ambient = ambient_strength * light_color;

	vec3 color = ambient * object_color;
	frag_color = vec4(color, 1.0);
}

这里添加了一个环境光强度,将环境光设置为光源亮度的十分之一,然后我们仅使用环境光照亮物体,得到的结果是这样的:

在这里插入图片描述

三、添加漫反射光照

漫反射表示粗糙物体表面向四面八方反射入射光的现象,这样不论你从哪个位置观察同一个点的光强都是一样的,也就是漫反射光跟观察者的方位无关。但是漫反射和光源的位置有关,对于某个 shading point ,其和光源连线与该 shading point 对应的物体表面的法线之间的夹角越小,漫反射光照越强,如下图:

img

这也很好理解, θ \theta θ 越小,物体表面接收到的光的竖直分量越多(水平分量不照亮物体)。因此,Phong 模型中对于漫反射部分的计算是这样的:

C d i f f u s e = k d C l i g h t m a x ( 0 , n ⃗ ⋅ l ⃗ ) C_{diffuse} = k_d C_{light}max(0,\vec n \cdot \vec l ) Cdiffuse=kdClightmax(0,n l )

各个分量表示的含义:

  • C d i f f u s e C_{diffuse} Cdiffuse 最终计算得到的漫反射颜色;
  • k d k_d kd 物体的漫反射系数(可以简单理解为物体的颜色)
  • C l i g h t C_{light} Clight 光源的颜色
  • n n n 物体表面的法线方向(需要归一化)
  • l l l 入射光的方向(便于计算一般指向光源)

因此,计算漫反射光照还需要的两个分量就是法线方向和入射光方向。

首先需要更新顶点数据,加入法线方向信息:新的顶点数据数组

然后重新指定定点布局,并在顶点着色器中进行布局指定:

Buffer_Layout layout2 = {
		{Shader_Data_Type::Float3, "a_Position"},
		{Shader_Data_Type::Float3, "a_normal"}
	};
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;

在 Phong 模型中,我们所有的光照计算都是在片元着色器中进行的,因此需要将顶点数据中的 normal 传到片元着色器中:

out vec3 v_normal;

void main()
{
	v_normal = a_normal;
	gl_Position = u_mvp * vec4(a_position, 1.0);
}

in vec3 v_normal;

最后就是根据之前的公式对漫反射光照进行计算,现在还缺少的变量是入射光的方向,这个也很好计算,只需要用光源的位置减去渲染目标点的位置即可,注意要使用同一个空间的坐标向量进行相减,这里我们均使用世界空间的坐标进行计算。

这里会用到 model 矩阵,所以我们将 model 矩阵作为 uniform 变量传入:

#version 330 core

layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;

out vec3 v_normal;
out vec3 v_world_pos;

uniform mat4 u_model;
uniform mat4 u_mvp;

void main()
{
		gl_Position = u_mvp * vec4(a_position, 1.0);
    	v_normal = a_normal;
    	v_world_pos = vec3(model * vec4(a_position, 1.0));
    
}

片元着色器中的计算如下:

#version 330 core

out vec4 frag_color;

in vec3 v_normal;
in vec3 v_world_pos;

uniform vec3 light_color;
uniform vec3 object_color;
uniform vec3 light_pos;

void main()
{
	float ambient_strength = 0.1;
	vec3 ambient = ambient_strength * light_color;

	vec3 light_dir = normalize(light_pos - v_world_pos);
	vec3 diffuse_color = light_color * object_color * max(0.0, dot(v_normal, light_dir));

	vec3 color = ambient * object_color + diffuse_color;
	frag_color = vec4(color, 1.0);
}

如果一切都没问题,运行结果是这样的:

在这里插入图片描述

要注意的点是,我们是在世界空间中计算的光照,但是我们并没有把法向量 normal 也转化到世界空间中,那么该如何转化呢?首先要明确一点的是,法线的变换和顶点是不同的,不能像顶点一样乘上变换矩阵转化到别的空间,因为可能会出现下面的情况:

img

法线向量只能保证方向的一致性,而不能保证位置的一致性,所以,所有线向量必须以面的形式进行变换,具体就是要乘变换矩阵的逆转置矩阵,推导如下:

如果我们用一个法向量n = [a, b, c, d] 来描述一个平面,对于平面上的任意点p = [x, y, z, 1] 都遵循如下等式:

ntp = ax + by + cz + d = 0

如果将平面上的点进行可逆变换,乘以个可逆矩阵 R 来得到变换后的点 p1, 假设该平面做相同的变换之后对应的法向量为 n1,法向量 n 可以通过乘变换矩阵 Q 来得到 n1,这里 Q 是未知的:

p1 = R p

n1 = Q n

然后根据等式 n1tp1 = 0, 可以解得矩阵 Q

(Q n)t (R p) = 0

ntQtR p = 0

同时又有ntp = 0,所以QtR = E (单位矩阵),因此 Qt = R-1 , Q = (R-1)t 也就是逆转置矩阵。

所以为了将法线也进行变换,我们需要计算原先变换矩阵的逆转置矩阵,注意矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。

计算逆转置矩阵并且在顶点着色器中进行声明:

		glm::mat4 normal_matrix = glm::transpose(glm::inverse(model));
		mvp = camera.get_view_projection_matrix() * model;
		box_shader.bind();
		box_shader.set_float3("light_color", glm::vec3(1.0f, 1.0f, 1.0f));
		box_shader.set_float3("object_color", glm::vec3(1.0f, 0.5f, 0.31f));
		box_shader.set_mat4("u_mvp", mvp);
		box_shader.set_mat4("u_model", model);
		box_shader.set_mat4("u_normal_matrix", normal_matrix);
		v_normal = mat3(u_normal_matrix) * a_normal;

四、添加高光反射光照

和漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,同时也决定于观察方向,比如眼睛是从什么方向看向这个着色点的。镜面光照决定于表面的反射特性。如果把物体表面设想为一面镜子,那么镜面光照最强的地方就是反射光所在的方向。

在这里插入图片描述

反射向量与观察方向的夹角越小,镜面光的作用就越大。

Phong 模型中,镜面高光的计算是这样的:

C s p e c u l a r = k s C l i g h t p o w ( m a x ( 0 , r ⃗ ⋅ v ⃗ ) , ρ ) C_{specular} = k_s C_{light}pow(max(0,\vec r \cdot \vec v), \rho) Cspecular=ksClightpow(max(0,r v ),ρ)

其中 ρ \rho ρ 是高光反射的反光度(Shininess),物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。

因此式中需要求解的向量有反射向量 r 和视线方向 v,反射向量 r 可以通过reflect 函数来实现,v则需呀获取视点方向,也就是相机的位置,因此需要声明视点方向的全局变量:

// shader
uniform vec3 view_pos;
//cpp
box_shader.set_float3("view_pos", camera.get_position());

然后就可以在片元着色器中进行高光计算了:

	vec3 view_dir = normalize(view_pos - v_world_pos);
	vec3 reflect_dir = reflect(-light_dir, normal);

	float specular_strength = 0.5;
	vec3 specular_color = specular_strength * light_color * pow(max(dot(view_dir, reflect_dir), 0.0), 35.0);
	vec3 diffuse_color = light_color * max(0.0, dot(normal, light_dir));

	vec3 color = (ambient + diffuse_color + specular_color) * object_color;

通过控制反光度,可以得到下面的效果:

img

将光源的位置实时变化,可以得到下面的结果:

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值