此文上接WebGL 三维方向光源, 如果没看请从那里开始。
上篇中说到方向光统一来自一个方向,在渲染前设置好方向。
如果代替方向而是从三维空间中选一个点当作光源, 然后在着色器中根据光源和表面位置计算光照方向的话,就是点光源了。
如果旋转上方的表面会发现,每个点都有一个不同的面到光源的矢量, 将这个矢量和法向量点乘后,表面上的每个点都会有一个不同的光照值。
让我们实现它吧。
首先需要一个光源位置
uniform vec3 u_lightWorldPosition;
然后需要计算表面的世界坐标,我们可以将位置和世界矩阵相乘得到…
uniform mat4 u_world;
...
// 计算表面的世界坐标
vec3 surfaceWorldPosition = (u_world * a_position).xyz;
然后可以计算出一个从表面到光源的矢量,用来模拟之前的方向光, 只是这次我们为表面上的每个点都计算了一个方向。
v_surfaceToLight = u_lightPosition - surfaceWorldPosition;
这是全部的内容
#version 300 es
in vec4 a_position;
in vec3 a_normal;
+uniform vec3 u_lightWorldPosition;
+uniform mat4 u_world;
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;
out vec3 v_normal;
+out vec3 v_surfaceToLight;
void main() {
  // 将位置和矩阵相乘
  gl_Position = u_worldViewProjection * a_position;
  // 重定向法向量并传递给片断着色器
  v_normal = mat3(u_worldInverseTranspose) * a_normal;
+  // 计算表面的世界坐标
+  vec3 surfaceWorldPosition = (u_world * a_position).xyz;
+
+  // 计算表面到光源的方向
+  // 传递给片断着色器
+  v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
}
在片断着色器中需要将表面到光源的方向进行单位化,
注意,虽然我们可以在顶点着色器中传递单位向量,
但是 varying 会进行插值再传给片断着色器,
所以片断着色器中的向量基本上不是单位向量了。
#version 300 es
precision highp float;
// 从顶点着色器中传入的值
in vec3 v_normal;
+in vec3 v_surfaceToLight;
-uniform vec3 u_reverseLightDirection;
uniform vec4 u_color;
// 定义一个传递到片段着色器的颜色变量
out vec4 outColor;
void main() {
  // 由于 v_normal 是可变量,所以经过插值后不再是单位向量,
  // 单位化后会成为单位向量
  vec3 normal = normalize(v_normal);
  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
-  float light = dot(v_normal, u_reverseLightDirection);
+  float light = dot(v_normal, surfaceToLightDirection);
  outColor = u_color;
  // 只将颜色部分(不包含 alpha) 和光照相乘
  outColor.rgb *= light;
}
然后需要找到 u_world 和 u_lightWorldPosition 的位置
-  var reverseLightDirectionLocation =
-      gl.getUniformLocation(program, "u_reverseLightDirection");
+  var lightWorldPositionLocation =
+      gl.getUniformLocation(program, "u_lightWorldPosition");
+  var worldLocation =
+      gl.getUniformLocation(program, "u_world");
设置它们
  // 设置矩阵
+  gl.uniformMatrix4fv(
+      worldLocation, false,
+      worldMatrix);
  gl.uniformMatrix4fv(
      worldViewProjectionLocation, false,
      worldViewProjectionMatrix);
  ...
-  // 设置光照方向
-  gl.uniform3fv(reverseLightDirectionLocation, normalize([0.5, 0.7, 1]));
+  // 设置光源位置
+  gl.uniform3fv(lightWorldPositionLocation, [20, 30, 50]);
这是结果
现在我们可以加一个叫做镜面高光的东西。
观察现实世界中的物体,如果物体表面恰好将光线反射到你眼前, 就会显得非常明亮,像镜子一样。
 
我们可以通过计算光线是否反射到眼前来模拟这种情况,点乘又一次起到了至关重要的作用。
如何测试呢?如果入射角和反射角恰好与眼睛和和光源的夹角相同,那么光线就会反射到眼前。
如果我们知道了物体表面到光源的方向(刚刚已经计算过了),
加上物体表面到视区/眼睛/相机的方向,再除以 2 得到 halfVector 向量,
将这个向量和法向量比较,如果方向一致,那么光线就会被反射到眼前。
那么如何确定方向是否一致呢?用之前的点乘就可以了。1 表示相符,
0 表示垂直,-1 表示相反。
所以首先我们需要传入相机位置,计算表面到相机的方向矢量, 然后传递到片断着色器。
#version 300 es
in vec4 a_position;
in vec3 a_normal;
uniform vec3 u_lightWorldPosition;
+uniform vec3 u_viewWorldPosition;
uniform mat4 u_world;
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;
varying vec3 v_normal;
out vec3 v_surfaceToLight;
+out vec3 v_surfaceToView;
void main() {
  // 将位置和矩阵相乘
  gl_Position = u_worldViewProjection * a_position;
  // 重定向法向量并传递到片断着色器
  v_normal = mat3(u_worldInverseTranspose) * a_normal;
  // 计算表面的世界坐标
  vec3 surfaceWorldPosition = (u_world * a_position).xyz;
  // 计算表面到光源的方向
  // 然后传递到片断着色器
  v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
+  // 计算表面到相机的方向
+  // 然后传递到片断着色器
+  v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
}
然后在片断着色器中计算表面到光源和相机之间的 halfVector,
将它和法向量相乘,查看光线是否直接反射到眼前。
// 从顶点着色器中传入的值
in vec3 v_normal;
in vec3 v_surfaceToLight;
+in vec3 v_surfaceToView;
uniform vec4 u_color;
out vec4 outColor;
void main() {
  // 由于 v_normal 是可变量,所以经过插值后不再是单位向量,
  // 单位化后会成为单位向量
  vec3 normal = normalize(v_normal);
+  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
+  vec3 surfaceToViewDirection = normalize(v_surfaceToView);
+  vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
  float light = dot(normal, surfaceToLightDirection);
+  float specular = dot(normal, halfVector);
  outColor = u_color;
  // 只将颜色部分(不包含 alpha) 和光照相乘
  outColor.rgb *= light;
+  // 直接加上高光
+  outColor.rgb += specular;
}
最后找到 u_viewWorldPosition 并设置它
var lightWorldPositionLocation =
    gl.getUniformLocation(program, "u_lightWorldPosition");
+var viewWorldPositionLocation =
+    gl.getUniformLocation(program, "u_viewWorldPosition");
...
// 计算相机矩阵
var camera = [100, 150, 200];
var target = [0, 35, 0];
var up = [0, 1, 0];
var cameraMatrix = makeLookAt(camera, target, up);
...
+// 设置相机位置
+gl.uniform3fv(viewWorldPositionLocation, camera);
这是结果
但,亮瞎了!
我们可以将点乘结果进行求幂运算来解决太亮的问题, 它会把高光从线性变换变成指数变换。
红线越接近顶部,我们加的光照就越多,通过求幂可以将高光的部分向右移动。
就把它叫做 shininess 并加到着色器中。
uniform vec4 u_color;
+uniform float u_shininess;
...
-  float specular = dot(normal, halfVector);
+  float specular = 0.0;
+  if (light > 0.0) {
+    specular = pow(dot(normal, halfVector), u_shininess);
+  }
点乘结果有可能为负值,将赋值求幂有可能会得到 undefined 的结果, 所以我们只将点乘结果为正的部分进行计算,其他部分设置为 0.0。
当然还要找到亮度的位置并设置它
+var shininessLocation = gl.getUniformLocation(program, "u_shininess");
...
// 设置亮度
gl.uniform1f(shininessLocation, shininess);
这是结果
最后想说的是光的颜色。
在此之前我们都是将 light 和 F 的颜色直接相乘,
如果想要有色光也可以为光照提供颜色。
uniform vec4 u_color;
uniform float u_shininess;
+uniform vec3 u_lightColor;
+uniform vec3 u_specularColor;
...
  // 只将颜色部分(不包含 alpha) 和光照相乘
*  outColor.rgb *= light * u_lightColor;
  // 直接和高光相加
*  outColor.rgb += specular * u_specularColor;
}
然后
+  var lightColorLocation =
+      gl.getUniformLocation(program, "u_lightColor");
+  var specularColorLocation =
+      gl.getUniformLocation(program, "u_specularColor");
和
+  // 设置光照颜色
+  gl.uniform3fv(lightColorLocation, normalize([1, 0.6, 0.6]));  // red light
+  // 设置高光颜色
+  gl.uniform3fv(specularColorLocation, normalize([1, 0.2, 0.2]));  // red light
接下来是 聚光灯.