颜色和光照
一、颜色的物理解释
颜色(英语:colour,color)又称色彩、色泽,是眼、脑和我们的生活经验对光的颜色类别描述的视觉感知特。这种对颜色的感知来自可见光谱中的电磁辐射对人眼视锥细胞的刺激。颜色是由光反射所产生的,这种反射是由物体的物理性质决定的,如光的吸收、发射光谱等。
二、计算机中的颜色
在计算机领域中,我们需要使用(有限的)数值来模拟真实世界中(无限)的颜色,所以并不是所有现实世界中的颜色都可以用数值来表示的。颜色可以数字化的由红色(Red)、绿色(Green)和蓝色(Blue)三个分量组成,它们通常被缩写为RGB。仅仅用这三个值就可以组合出任意一种颜色。红绿蓝三种颜色被称之为三原色。下图是表示RGB色彩的立方体:
三、人眼观察颜色的原理
3.1 人眼的生理构造
人眼中的视锥细胞和视杆细胞都能感受颜色,一般人眼中有三种不同的视锥细胞:第一种主要感受黄绿色,它的最敏感点在565纳米左右;第二种主要感受绿色,它的最敏感点在535纳米左右;第三种主要感受堇紫色,其最敏感点在420纳米左右。视杆细胞只有一种,它的最敏感的颜色波长在蓝色和绿色之间。
每种视锥细胞的敏感曲线大致是钟形的,视锥细胞依照感应波长不同由长到短分为L、M、S三种。因此进入眼睛的光一般相应这三种视锥细胞和视杆细胞被分为4个不同强度的信号。
同一种颜色在不同的亮度中会产生不同的颜色感。这个现象的原因是我们的眼睛中除了有锥状细胞外还有可以感光的杆状细胞。杆状细胞虽然一般被认为只能分辨黑白,但它们对不同的颜色的灵敏度是略微不同的,因此当光暗下来的时候,杆状细胞的感光特性就越来越重要了,它可以改变我们对颜色的感觉。
3.2 物体的反射
我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的(Reflected)颜色。换句话说,那些不能被物体所吸收(Absorb)的颜色(被拒绝的颜色)就是我们能够感知到的物体的颜色。例如,太阳光能被看见的白光其实是由许多不同的颜色组合而成的(如下图所示)。如果我们将白光照在一个蓝色的玩具上,这个蓝色的玩具会吸收白光中除了蓝色以外的所有子颜色,不被吸收的蓝色光被反射到我们的眼中,让这个玩具看起来是蓝色的。下图显示的是一个珊瑚红的玩具,它以不同强度反射了多个颜色。
四、图形学中颜色的计算
在图形学中,只需要把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。可以定义物体的颜色为物体从一个光源反射各个颜色分量的大小,比如用一个白色的光源来照一个珊瑚红的物体:
glm::vec3 light_color(1.0f, 1.0f, 1.0f);
glm::vec3 coral_color(1.0f, 0.5f, 0.31f);
glm::vec3 result = light_color * coral_color; // = (1.0f, 0.5f, 0.31f);
物体的颜色表示了该物体对红色光的反射率为1,也就是全部反射掉,对绿色光的反射率为0.5,也就是有一半的绿光被反射,蓝色光的反射率为0.31,有0.31的蓝色光被反射,最终反射的光线就有 1的红光,0.5的绿光和0.31的蓝光,正好就是反射光线对应的珊瑚红色。
五、创建光照场景
我们使用之前创建的立方体箱子作为光源的投射对象,并在3D场景中添加一个光源,简单起见仍然用一个立方体来表示。
首先创建一个顶点数组和顶点缓冲区对象,并用之前的箱子数据填充(这里我使用了抽象的vertex array 和 buffer 类,详见Kaoru-misono/OpenGL):
std::shared_ptr<Vertex_Array> box_vertex_array = std::make_shared<Vertex_Array>();
std::shared_ptr<Vertex_Buffer> box_vertex_buffer = std::make_shared<Vertex_Buffer>(box_vertices, sizeof(box_vertices));
box_vertex_buffer->set_layout(layout2);
box_vertex_array->add_vertex_buffer(box_vertex_buffer);
创建一个简单的顶点着色器用于给箱子的顶点变换:
#version 330 core
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec2 a_texcoord;
uniform mat4 u_mvp;
void main()
{
gl_Position = u_mvp * vec4(a_position, 1.0);
}
然后我们给光源对应的箱子创建一个新的顶点数组,但是顶点缓冲区仍然用刚才创建好的箱子的顶点数据,因为两个箱子的顶点数据是相同的,没有必要再创建一个顶点缓冲区。
std::shared_ptr<Vertex_Array> light_VAO = std::make_shared<Vertex_Array>();
light_VAO->add_vertex_buffer(box_VBO);
最后我们需要计算物体的颜色,因此创建一个片元着色器:
#version 330 core
out vec4 frag_color;
uniform vec3 light_color;
uniform vec3 object_color;
void mian()
{
frag_color = light_color * object_color;
}
创建好shader之后,在循环中对两个颜色对应的全局变量进行设置:
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));
为了能够让光源的颜色不受干扰,我们单独创建一个顶点和片元着色器来绘制光源对应的箱子:
#version 330 core
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec2 a_texcoord;
uniform mat4 u_mvp;
void main()
{
gl_Position = u_mvp * vec4(a_position, 1.0);
}
#version 330 core
out vec4 frag_color;
void mian()
{
frag_color = vec4(1.0);
}
使用这个灯立方体的主要目的是为了让我们知道光源在场景中的具体位置。通常在场景中定义的一个光源的位置只是一个空间坐标,它并没有视觉意义。为了让我们能够直观观察到光源,将表示光源的立方体绘制在与光源相同的位置。本节中我们使用一个单独的片元着色器绘制光源,让它一直处于白色的状态,不受场景中的光照影响。
首先声明光源在场景中的位置:
glm::vec3 light_pos(1.2f, 1.0f, 2.0f);
把我们用作光源的箱子平移到该位置,并且将其的大小缩小一点:
model = glm::mat4(1.0f);
model = glm::translate(model, light_pos);
model = glm::scale(model, glm::vec3(0.2f));
在 Render Loop 中,我们绘制两个立方体,注意要使用不同的顶点数组来绘制:
box_VAO->bind();
model = glm::mat4(1.0f);
mvp = camera.get_view_projection_matrix() * model;
box_shader.bind();
box_shader.set_float4("light_color", glm::vec4(1.0f, 1.0f, 1.0f, 1.0f));
box_shader.set_float4("object_color", glm::vec4(1.0f, 0.5f, 0.31f, 1.0f));
box_shader.set_mat4("u_mvp", mvp);
glDrawArrays(GL_TRIANGLES, 0, 36);
light_VAO->bind();
model = glm::mat4(1.0f);
model = glm::translate(model, light_pos);
model = glm::scale(model, glm::vec3(0.2f));
mvp = camera.get_view_projection_matrix() * model;
light_shader.bind();
light_shader.set_mat4("u_mvp", mvp);
glDrawArrays(GL_TRIANGLES, 0, 36);
为了能够观察到整个场景,我们对相机的初始位置和朝向进行了重新设置:
Perspective_Camera camera(glm::vec3(0.0f, 2.0f, 5.0f), glm::vec3(-20.0f, -80.0, 0.0f));
这里使用了位置和欧拉角来初始化相机,运行代码得到的结果应该是这样的: