在WebGL中,颜色用RGBA表示,每个值的取值范围从0到1,对应我们常用的取值范围0到255。例如在着色器中,想输出一个黑色:
void main() {
FragColor = vec4(0.0, 0.0, 0.0, 1.0); // rgba(0, 0, 0, 1)
}
在真实世界中,我们肉眼看到的颜色,往往不是物体真正的颜色。真实世界中物体的反射的颜色一般会与环境有关,经过光线照射(太阳光,灯光),物体会反射与其本身颜色有一定差异的颜色,在白天和夜晚由于光线的强度不同,物体反射的颜色一般也不同。所以,了不起的前辈们用以下公式模拟计算出物体最终反射的颜色:
FragColor = ObjectColor * LightColor; // 将对应的rgba分量相乘,得到FragColor的rgba值
FragColor
表示最终颜色,ObjectColor
表示物体本身的颜色,LightColor
表示光线的颜色。这个公式用于计算一个物体在某种颜色的灯光下反射的颜色,不同的灯光颜色会产生不同的结果颜色。例如如果物体是黑色的,那么它会吸收所有颜色的光,最终反射出黑色:
FragColor = vec4(0.0, 0.0, 0.0, 1.0) * LightColor; // 因为rgb为0,所以FragColor的rgb为0,显示为黑色。
再比如一个红色的物体(rgba(1, 0, 0, 1)),在太阳光下(太阳光我们认为其rgba为(1, 1, 1, 1)),显示为红色:
FragColor = vec4(1.0, 0.0, 0.0, 1.0) * vec4(1.0, 1.0, 1.0, 1.0); // FragColor = vec4(1.0, 0.0, 0.0, 1.0)
FragColor = ObjectColor * LightColor
在图形领域中,被称作颜色的反射定律。这种方式可能不能完全模拟真实世界的所有颜色,但是已经足够我们用来模拟一个非常真实的世界了。
图片来源于LearnOpenGL_CN
冯氏光照模型是处理现实世界复杂光照的一种模型,冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
利用冯氏光照模型,我们可以创建出非常有意思的场景了,接下来跟着我,来绘制一个光照下的立方体吧。
为了实现环境光照,只需要设置一个环境光因子,用环境光因子乘以光照,然后再将结果乘以物体颜色得到最终的片段着色器输出颜色。片段着色器glsl如下:
#version 300 es
precision highp float;
out vec4 FragColor;
uniform vec3 u_objColor; // 物体颜色
uniform vec3 u_lightColor; // 光照颜色
float ambientStrength = 0.1; // 环境光因子
void main() {
vec3 ambient = ambientStrength * u_lightColor;
vec3 result = u_objColor * ambient;
FragColor = vec4(result, 1.0);
}
我后续的代码都是使用的WebGL2,与WebGL有一点差异,但是差异不是很大,不会WebGL2的朋友不必担心。作为冯氏光照的第一阶段,环境光照是是很弱的,所以这里环境因子我设置为0.1,只给物体提供一丢丢颜色。
接下来绘制一个立方体。立方体顶点数据可以在这里获得。接下来就是创建VAO(Vertext Array Data)、VBO(Vertex Array Buffer),给着色器传递数据了,这一过程比较简单我就不详细写下来了,可以参考源码:https://github.com/pengfeiw/webgl2-demos/blob/master/src/demos/theroyOfLight/phongLighting.ts。
const vertex_source =
`#version 300 es
in vec3 a_pos;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main() {
gl_Position = projection * view * model * vec4(a_pos, 1.0);
}
`;
const fragment_source =
`#version 300 es
precision highp float;
out vec4 FragColor;
uniform vec3 u_objColor; // 物体颜色
uniform vec3 u_lightColor; // 光照颜色
float ambientStrength = 0.1; // 环境光因子
void main() {
vec3 ambient = ambientStrength * u_lightColor;
vec3 result = u_objColor * ambient;
FragColor = vec4(result, 1.0);
}
`;
const draw = (gl: WebGL2RenderingContext) => {
const shader = new Shader(gl, vertex_source, fragment_source);
// vao
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// vbo
const vbo = gl.createBuffer();
const posAttLocation = gl.getAttribLocation(shader.program, "a_pos");
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticesData), gl.STATIC_DRAW);
gl.enableVertexAttribArray(posAttLocation);
gl.vertexAttribPointer(posAttLocation, 3, gl.FLOAT, false, 0, 0);
// uniform
shader.useProgram();
const model = mat4.create();
shader.setMat4("model", model);
shader.setFloat3("u_objColor", 0.36, 0.42, 0.60);
shader.setFloat3("u_lightColor", 1.0, 1.0, 1.0);
const camera = new Camera([0, 1, 4]);
camera.mouseSensitivity = 0.04;
var deltaTime = 0.01;
var lastFrame = 0;
const drawScene = (time: number) => {
resizeCanvas(gl);
const currentFrame = time;
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
gl.enable(gl.DEPTH_TEST);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindVertexArray(vao);
shader.useProgram();
const view = camera.getViewMatrix();
shader.setMat4("view", view);
const projection = mat4.perspective(mat4.create(), glMatrix.toRadian(camera.zoom), gl.canvas.width / gl.canvas.height, 0.1, 100);
shader.setMat4("projection", projection);
gl.drawArrays(gl.TRIANGLES, 0, 36);
requestAnimationFrame(drawScene);
};
window.addEventListener("keydown", (event: KeyboardEvent) => {
switch (event.key) {
case "w":
camera.processKeyboard(Camera_Movement.FORWARD, deltaTime * 0.001);
break;
case "s":
camera.processKeyboard(Camera_Movement.BACKWARD, deltaTime * 0.001);
break;
case "a":
camera.processKeyboard(Camera_Movement.LEFT, deltaTime * 0.001);
break;
case "d":
camera.processKeyboard(Camera_Movement.RIGHT, deltaTime * 0.001);
break;
default:
break;
}
});
window.addEventListener("wheel", (event: WheelEvent) => {
camera.processMouseScroll(event.deltaY * 0.01);
});
window.addEventListener("mousemove", (event: MouseEvent) => {
camera.processMouseMovement(event.movementX, event.movementY);
});
requestAnimationFrame(drawScene);
};
代码里,我使用了我自己封装的Camera和Shader,这两部分我都详细讲述过,请参考:模拟一个摄像机、绘制一个三角形(shader程序创建)。
设置物体颜色与光照颜色:
shader.setFloat3("u_objColor", 0.36, 0.42, 0.60); // 淡蓝紫色
shader.setFloat3("u_lightColor", 1.0, 1.0, 1.0); // 白色光
绘制结果如下,立方体仅仅有一点点颜色,表现很暗:
冯氏光照第一阶段:环境光照对物体颜色的影响
作为冯氏光照的第二阶段,漫反射光照对人的视觉产生的效果是最为显著的,也是最有意思的。请看下图:
图片来源于Learn_OpenGL_CN官网:漫反射光照示意图
光线逆方向与物体表面法向量有一个夹角,当夹角为0时,此时光照直射物体表面,光照强度最大,物体越亮,当夹角大于90度时,光照最小,物体表面颜色最暗。我们知道点积公式如下:
a·b = |a|*|b|*cos(angle)
由点积公式得到,当向量夹角为0时,点积最大,当夹角为90度时,点积为0。所以我们可以利用点积公式计算物体最终显示的颜色。要实现漫反射光照效果,首先我们需要知道每个顶点的法向量,由于立方体比较简单,我直接给出立方体的法向量数据,可以在这里获得顶点法向量的数据。在顶点着色器中增加法向量属性a_normal
, 并利用v_normal
传递给片段着色器
...
in vec3 a_normal;
out vec3 v_normal;
void main() {
...
v_normal = a_normal;
}
在片段着色器中需要增加光照方向的全局变量:
uniform vec3 u_lightDirection; // 光照方向
in vec3 v_normal;
因为计算夹角,使用的是光照方向的相反方向,所以需要先计算光照的逆方向向量,并标准化向量:
vec3 lightDirectionReverse = normalize(vec3(-u_lightDirection.x, -u_lightDirection.y, -u_lightDirection.z));
vec3 normal = normalize(v_normal);
计算点积, 点积如果为负,取0值:
float diffuseStrength = max(dot(normal, lightDirectionReverse), 0.0);
计算漫反射分量,并更新输出颜色:
vec3 diffuse = diffuseStrength * u_lightColor;
vec3 result = u_objColor * (ambient + diffuse);
FragColor = vec4(result, 1.0);
完整的glsl如下:
// 顶点着色器
#version 300 es
in vec3 a_pos;
in vec3 a_normal;
out vec3 v_normal;
uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;
void main() {
gl_Position = projection * view * model * vec4(a_pos, 1.0);
v_normal = a_normal;
}
// 片段着色器
#version 300 es
precision highp float;
in vec3 v_normal;
out vec4 FragColor;
uniform vec3 u_objColor; // 物体颜色
uniform vec3 u_lightColor; // 光照颜色
uniform vec3 u_lightDirection; // 光照方向
float ambientStrength = 0.1; // 环境光因子
void main() {
// 环境光照
vec3 ambient = ambientStrength * u_lightColor;
// 漫反射
vec3 lightDirectionReverse = normalize(vec3(-u_lightDirection.x, -u_lightDirection.y, -u_lightDirection.z));
vec3 normal = normalize(v_normal);
float diffuseStrength = max(dot(normal, lightDirectionReverse), 0.0);
vec3 diffuse = diffuseStrength * u_lightColor;
vec3 result = u_objColor * (ambient + diffuse);
FragColor = vec4(result, 1.0);
}
在绘制代码,我们需要增加传递法向量数据,创建缓冲传递数:
const normalVbo = gl.createBuffer();
const normalAttLocation = gl.getAttribLocation(shader.program, "a_normal");
gl.bindBuffer(gl.ARRAY_BUFFER, normalVbo);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
gl.enableVertexAttribArray(normalAttLocation);
gl.vertexAttribPointer(normalAttLocation, 3, gl.FLOAT, false, 0, 0);
设置光照方向:
shader.setFloat3("u_lightDirection", 1.0, -0.5, -1.0);
然后,你应该能绘制出如下的情景,不同的面颜色亮度不一样:
冯氏光照的最后一个阶段,只有把镜面高光加入到我们的绘制场景中,才算一个完整的冯氏光照。
在现实世界中,在一个阳光明媚的下午,你看向一个表面光滑的物体,比如镜子,站在某个位置,你会被物体表面反射的强光闪到眼睛,因为某一块特别的亮,这种现象叫做镜面光照,这就是冯氏光照第三阶段需要处理的情况,与漫反射一样,这一阶段也是利用了点积的思想。
图片来源于LearnOpenGL_CN
从图中可以看到,光线照射到物体表面,会反射出一个光线,我们称这个为反射光,反射光与我们视线形成一个夹角,当这个夹角为0时,我们就会产生刺眼的效果,所以夹角越小,镜面光照的影响越大。
为了计算镜面光照,我们需要以下两个方向向量:
视线的方向可以通过片段位置和摄像机的位置计算得出,反射光的方向可以通过法向量和光照方向计算得出。在片段着色器中增加摄像机位置全局变量:
uniform vec3 viewPos; // 摄像机位置
shader.setFloat3("viewPos", camera.position[0], camera.position[1], camera.position[2]);
将片段位置从顶点着色器中传递到片段着色器中:
// 顶点着色器
out vec3 v_pos;
void main() {
v_pos = (model * vec4(a_pos, 1.0)).xyz;
}
// 片段着色器
in vec3 v_pos;
计算镜面光照,利用glsl中的reflect
函数可以直接计算反射向量:
// 镜面光照
vec3 viewDir = normalize(viewPos - v_pos); // 观察方向向量
vec3 reflectDir = reflect(u_lightDirection, normal); // 光的反射向量,利用reflect函数计算
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 30.0); // 30.0是高光的反光度
vec3 specular = specularStrength * spec * u_lightColor;
上面有一个物体反光度的概念,这里设置为30.0,一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。我后面介绍物体材质时,会详细讲解反光度。
更新FragColor:
vec3 result = u_objColor * (ambient + diffuse + specular);
FragColor = vec4(result, 1.0);
如果你的代码, 没有错误,应该能看到如下的反光效果:
完整代码请参阅:theroyOfLight/phongLighting.ts。
(完)